Commit Graph

988 Commits

Author SHA1 Message Date
sylph-decoder
54a719c286 re: stripping additive padding is exact -- controlled, and only one side needs it
The page recorded the substituted-versus-additive distinction as unverified, with
the port controlling it. It passes.

A real music and SFX bed of 137.37 s, itself carrying 454 genuine zero runs --
which is what makes it an honest control -- had 1149 holes inserted at 8.37
gaps/s to +9.9 percent length, matching the measured ALSA profile, then stripped:

  original vs itself (ceiling)   r 1.000  lag 0.0 s  margin +0.141
  padded vs original             r 0.436  lag -12.2  margin +0.006
  stripped vs original           r 1.000  lag  0.0   margin +0.142

Two things beyond the yes. It runs the inference forwards: padding at this
profile puts correlation squarely in the known-absent regime on a file whose
contents are controlled, so the earlier captures were unusable for the reason
claimed rather than for some other one -- until now that was reasoning backwards
from a failure to a cause. And only one side needs stripping, since the stripped
capture matches the UNSTRIPPED source at the ceiling, so a capture needs no
preprocessing before being handed over and there is no shared step to get out of
sync on.

The danger is recorded as the part to repeat: stripping removes genuine silence
too and cannot tell the two apart, so it is exact on additive ALSA padding and
vandalism on a PulseAudio monitor capture where the silence replaced real audio.
Running it on the wrong artefact would look like it worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 17:05:37 +00:00
sylph-decoder
798d7a2d4e handoff: deliver the centre-channel result and the S00A capture that is not taken
Records the port s measurement on the page they read, because it closes the last
open question and my own pages carried the hypothesis it settles. Keeps all
three limits as they stated them, including that streams 2 and 3 are
indistinguishable to the instrument so no selection rule is vindicated, and that
the one-of-three-streams warning stands with its character changed rather than
its colour.

Also names the capture that would strengthen it most and says plainly that I have
not taken it: S00A rather than a longer ADV, why it is structurally different,
and what it costs -- a driven rendered run, so no --gpu=null and the additive
padding comes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 17:01:44 +00:00
sylph-decoder
8841fe9c91 re: the voice dialogue is in the CENTRE channel -- measured, not inferred
The 5.1 reading of a voice cue s three concurrent streams was recorded here as a
hypothesis with counter-evidence attached. It is now answered, by the port
fitting the disc s decoded streams against a clean capture of the game s own
6-channel output, with the instrument controlled first (known-present margin
+0.248, known-absent +0.005).

Speech-band margins put streams 2 and 3 at +0.305 and +0.307 on FC, r = 0.989,
above the known-present control, while stream 1 sits in the noise on every
channel. The low band mirrors it exactly: the movie bed at 0.76 to 0.84 on the
four corners against 0.32 on FC. Dialogue in the centre, bed in the corners.

The hypothesis was right for a reason the file could never have supplied.
ChannelMask reads 0x0002 on all three streams, so the header is not merely
unhelpful, it is actively misleading -- refusing to call it 5.1 from the header
was correct, and the oracle answered what the header could not.

Three limits recorded as the measurer stated them: streams 2 and 3 are
indistinguishable to this instrument, so no rule for choosing between them is
vindicated; the one-of-three-streams warning stands, since nothing says what
streams 1 and 3 contribute; and the reach is 59.7 s of a 137 s movie, one run,
one asset.

Also records the capture that would strengthen it most and why it is not taken:
S00A rather than a longer ADV, because its second full-length stream is digital
silence where ADV s is a 0.60x copy, so a structurally different movie would
agree. Reaching it needs a driven rendered run -- S00A starts about 4.5 s after A
on the save slot -- so it cannot use --gpu=null and will carry the additive
padding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 17:01:26 +00:00
sylph-decoder
18d4fe61f4 re: substituted silence destroys information, additive padding does not
The distinction is the port s and it is sharper than the framing this page had.

PulseAudio s monitor SUBSTITUTES: audio that existed is replaced by silence to
keep the wall clock, so information is destroyed and deleting the holes only
compresses time unevenly. Xenia s padding is ADDITIVE: the silence is inserted
between samples the guest emitted, so nothing is lost and every real sample is
present and in order.

So stripping all-channel-zero runs from an ALSA-tee capture is exact rather than
a repair, which means even the 0.70x rendered capture at 9.98 percent padding is
usable for correlation, where none of the PulseAudio-monitor captures ever were
however they were tuned. Recorded as unverified: the port is controlling it by
padding a known source to match and checking the stripped result correlates back.

Consequence for check-capture recorded too: its silence and gap-rate rule was
built when only damage existed and cannot distinguish genuine emulator padding
from capture damage, so a FAIL on an ALSA-tee capture is a statement about the
recording path rather than the file s usability.

Also promotes the runaway guard to a first-class CONTAINER-NOTES entry at the
port s request -- 7.34 GB in 50 seconds at about 250x real time is not a
footnote -- and adds --gpu=null there, which is what takes the guest from 0.70x
to 0.96x and stops the padding, with its two caveats: no video for provenance,
and runs die at about 70 s with PM4_DRAW_INDX failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:56:32 +00:00
sylph-decoder
41b47fa281 re: --gpu=null gives a clean audio capture -- 0.31 percent silence, 0.01 gaps/s
The residual padding in the ALSA tee was the guest running at 0.70x real time,
and the dominant load is llvmpipe software rendering -- which an audio capture
does not need at all.

  PulseAudio monitor, xenia default             39.3 percent silence  30.5 gaps/s
  ALSA tee to paced slave, rendered              9.98 percent silence  8.37 gaps/s
  ALSA tee to paced slave, --gpu=null            0.31 percent silence  0.01 gaps/s

One gap in 67.7 s. Six distinct channels, no duplicates, sensible peaks. For
scale the port s genuine music bed control measures 1.1 percent silence at 3.3
gaps/s, so this capture is cleaner than their known-good reference.

Control that the run is still comparable: ADV s three XMA contexts appear in the
--gpu=null log, so the movie s voice is decoding exactly as in a rendered boot.
That is also better provenance for an audio question than screenshots were, since
it evidences the thing being recorded rather than what was on screen.

Records the full working recipe. Caveat: --gpu=null means no video, so
screen-based provenance is unavailable and it is only appropriate when the
question is about audio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:52:51 +00:00
sylph-decoder
7ff56fee3f notes: point the container page at the working audio-capture route
The entry already warned that run-canary is silent twice over, but stopped
there -- and fixing only the mutes still yields an unfaithful capture, because a
PulseAudio null sink s monitor is wall-clocked and invents silence when the
client is late. Adds the pointer to the ALSA file-tee page, with the two things
a reader needs before starting: the slave must pace or the tee free-runs at about
250x real time, and a size guard is not optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:51:14 +00:00
sylph-decoder
5d3afc526d re: the ALSA tee measured on Canary -- faithful now, but the guest still pads
150 s boot with --apu=alsa --mute=false, tee in front of the paced pulse slave.

  PulseAudio monitor, default 5.3 ms   39.3 percent silence   30.5 gaps/s
  PulseAudio monitor, 200 ms           15.6 percent silence    3.5 gaps/s
  PulseAudio monitor, 500 ms           50.1 percent silence    1.3 gaps/s
  ALSA tee to paced pulse slave         9.98 percent silence   8.37 gaps/s

106.2 s captured over about 151 s of wall clock, i.e. 0.70x real time: the file
is SHORT rather than gap-riddled, which is the intended trade. Six distinct
channels, no duplicates, sensible peaks.

A format trap worth recording: xenia s ALSA driver is SND_PCM_FORMAT_FLOAT_LE and
its log confirms 6 channels, so the raw tee is float32 and not s16. Reading it as
s16 yields a plausible-looking file whose giveaway is peaks alternating exactly
-0.00 / -4.82 across channels -- the two halves of each float landing in
alternate channels. I measured it wrongly that way first.

The residual 10 percent silence is not removed, but its meaning has changed. It
is no longer invented by PulseAudio s monitor; the tee records exactly what Xenia
wrote, and Xenia wrote silence, because its writer thread pads whenever the guest
has not filled the ring. So the capture is faithful -- every sample in it is a
sample the emulator emitted -- while the emulator is still padding, because the
guest runs at about 0.7x real time here. No capture method can remove that.

So this is a 3.9x improvement in silence and a change of attribution, not a clean
capture. At 9.98 percent and 8.37 gaps/s it sits right on the port s fail bar,
and should not be treated as an oracle without saying which side of the line it
fell on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:49:17 +00:00
sylph-decoder
12146ca11d re: capture audio through an ALSA file tee, not a PulseAudio monitor
The human identified that both agents were fighting the wrong subsystem, and
testing it here confirms the diagnosis and finds the limit.

A PulseAudio null sink s MONITOR is sampled on a wall clock: when the client is
late PulseAudio does not wait, it emits silence to keep its own timeline. So the
39.3 percent silence in the take-2 capture was never audio that went missing, it
was silence PulseAudio invented -- which is why PULSE_LATENCY_MSEC gave a
non-monotonic curve and never won. The instrument was wrong, not mistuned.

ALSA s file plugin has no clock; it tees exactly what the client writes, so a
slow producer yields a shorter file rather than a gap-riddled one. Control with
six distinct tones: 12.000 s against a 12.000 s source, 0.00 percent silence,
zero gaps, no duplicate channels. Channel order comes out as ALSA s
FL FR BL BR FC LFE rather than WAV s FL FR FC LFE BL BR -- deterministic and
invertible, not data loss.

Three configuration traps recorded in the order they bite: ALSA_CONFIG_PATH
replaces the whole config so the stock one must be included; but WITH that
include a pcm.!default override silently does not take, in either the inline or
the alias form, so the slave must be declared with an inline plugin type and no
include; and a pipe to head SIGPIPEs the producer before it writes, which looks
exactly like a broken config.

And the limit the proposer honestly flagged, now measured: a bare file tee is not
enough for Xenia, because its ALSA writer thread pads silence whenever the ring
buffer is empty (alsa_audio_driver.cc:359). Against a device that never blocks it
free-ran at about 250x real time -- 7.34 GB, 12746 s of nominal audio, in 50 s of
wall clock, nearly all driver-generated silence. Killed and deleted; it would
have filled the disk.

The configuration that satisfies both constraints is a tee in FRONT of a paced
slave: type file with slave.pcm { type pulse }. The file plugin captures what the
client writes and the slave supplies the clock, so the wall-clock silence
insertion happens downstream of the capture point. Control through that exact
config: 12.000 s, 0.00 percent silence, zero gaps.

Consequences for verification: short file becomes the failure mode, so a capture
check needs an expected-duration test alongside silence and gap rate, and a
runaway guard is not optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:47:31 +00:00
sylph-decoder
d75b3a752d re: the buffer/dropout relationship is not monotonic, and a gap-rate bar misses it
Extends the latency finding with a third point, and it changes the advice.

  xenia default (~5.3 ms)   347.5 s   39.3 percent silence   30.5 gaps/s   3.94 ms median
  PULSE_LATENCY_MSEC=200     88.0 s   15.6 percent silence    3.5 gaps/s  37.33 ms median
  PULSE_LATENCY_MSEC=500     87.9 s   50.1 percent silence    1.3 gaps/s 346.67 ms median

200 ms is 2.5x better than the default; 500 ms is worse than either. Raising the
buffer keeps cutting the gap RATE while total silence bottoms out at 200 ms and
then doubles, because an over-large buffer starves in a few enormous holes rather
than many small ones.

That is also a warning about the metric. The port s check-capture bar is 20
gaps/s, derived from sound controls -- starved 32.9, genuine music bed 3.3, voice
track 0.03. The 500 ms file scores 1.3 gaps/s, better than a real music bed,
while being 50 percent silence: a gap-rate test alone would pass the worst
capture of the three. It needs a total-silence companion. Same shape as the
defect that made a per-channel level table useless -- one number that cannot see
the failure mode next door.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:35:46 +00:00
sylph-decoder
76d19f8551 re: the capture dropouts are largely a client-buffer size, not the guest running slow
Withdraws my conclusion that the monitor-sink capture route cannot be fixed by
configuration, and with it the claim that only an in-emulator tap would work.

The container has no audio hardware -- no /proc/asound/cards, no /dev/snd, no
asound.conf -- so PulseAudio s stock default.pa module-always-sink supplies a
null sink, whose whole purpose is to exist when there is no device. A null sink
has no hardware clock: it is timer-driven, and anything the client fails to write
in time becomes silence in the monitor. That much was right.

What was wrong was inferring from it that the holes mean the guest runs below
real time. The alternative was never tested: xenia asks SDL for channel_samples_
= 256, which is 5.33 ms at 6 channels, and daemon.conf here is stock with no
fragment tuning. PULSE_LATENCY_MSEC overrides what SDL s PulseAudio backend
requests.

Measured, same title and sink and parec invocation:

  xenia default (~5.3 ms)   347.5 s   39.3 percent silence   30.5 gaps/s
  PULSE_LATENCY_MSEC=200     88.0 s   15.6 percent silence    3.5 gaps/s

An 8.7x reduction from one environment variable. Against the port s controls --
starved 32.9 gaps/s, genuine music bed 3.3, voice track 0.03, bar at 20 -- the
default is squarely starved and 200 ms lands at the level of real content.

Not yet a clean bill of health: the runs are not like-for-like at 88 s against
347 s, and the short one covers the splash logos where silence is real. What is
established is direction and scale. The consequence that matters is that the
capture route should be retried at raised latency before anyone spends a session
on a Canary rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:34:34 +00:00
sylph-decoder
3d4310bc05 handoff: tell the port the settle-time numbers carry an unmeasured real-time factor
The pulse-period argument for trusting the settle-time run is withdrawn, and the
port authors from those numbers so it belongs on the page they read. The plate
delay survives because it agrees with three independent prior readings; the menu
build-in and B-to-title are anchored by nothing, so a few per cent of emulator
slowdown sits inside them undetected. That is a second, independent reason to
treat those two as provisional beyond their being one-run figures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:28:23 +00:00
sylph-decoder
52a2eae4bf re: withdraw the pulse-period argument, and the shared WAV declares itself empty
Two corrections to my own recent work, both prompted by the port checking it.

First, the settle-time page argued the plate delay discrepancy was an instrument
artefact on two legs, and one of them is withdrawn. It said the plate pulse
period acts as an internal clock for presentation rate and measured 2.369 s
against the corpus s 2.3. That estimate rests on ONE interval between two
distinct troughs at a 125 ms sample interval -- uncertainty 0.177 s or 6.7
percent -- and trough-picking on a noisy plateau is fragile enough that
re-running it gives 2.628 s, because an adjacent local minimum had been counted
as a separate trough. Against the corpus s 2.24 that is +17.3 percent, about two
sigma. So the pulse period does not show the run at normal speed; it is too weak
to show anything, and cannot resolve a real-time factor below about 7 percent.

The conclusion survives on the other leg, which is the sound one: the
content-measured 2.247 s agrees with three independent prior readings
(2.13 / 2.132 / 2.138), and both its landmarks are sharp content transitions
rather than a trough on a plateau. A 17 percent slowdown would have put it at
2.49 s.

What that leaves open matters because the port authors from these numbers: the
run carries an unmeasured real-time factor under about 7 percent. The plate delay
is anchored by agreement with prior runs; the menu build-in and B-to-title are
anchored by nothing, so that is a second reason to treat them as provisional.

Second, the shared capture is worse than truncated: parec writes the WAV header
with zero sizes and patches them on clean exit, so the mid-write copy has RIFF
size 8 and data size 0 against 183 MB of actual bytes. Python s wave module
refuses to open it; ffmpeg and ffprobe recover by scanning and report a plausible
duration, which is exactly why it went unnoticed -- the lenient reader hid it.

Also corrects the attribution of the starvation numbers: 39.3 percent and
16680453 frames were measured on the finished local recording, not on the shared
artefact. The port measured the shared copy and got 35.6 percent and 15289876
frames, with burst and gap medians agreeing to 0.1 ms. The diagnosis is
unaffected but a number must say which artefact it came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:28:09 +00:00
sylph-decoder
64b30969d5 notes: build-canary points at a source root that does not exist here
Recorded as a blocker rather than worked around, because it changes what the
next session can plan.

The faithful-capture route is an internal tap at SDLAudioDriver::SubmitFrame,
which receives exactly frame_size_ bytes of the guest s own frame in guest order
with no wall clock in the loop. A cvar-gated WAV writer there would record what
the guest PRODUCED rather than what a device CONSUMED, so it would be gap-free
however slowly the emulator runs -- which is precisely the defect that made both
ADV captures unusable.

The change is small. The build is not. build-canary builds
${PROJECT_DIR:-/work}/xenia-canary, which does not exist in this container; the
source is at /canary. The warm 235 MB tree at /sylph-home/re/canary-build is
configured with CMAKE_HOME_DIRECTORY=/work/xenia-canary, also missing, and its
build-Release.ninja carries no per-file rules -- it re-runs CMake first, and that
reconfigure fails on the absent root. So any Canary change is a full reconfigure
against /canary plus a full compile, at SYLPH_JOBS=4 on a box sitting at about
700 MB free with a documented history of full-parallel builds OOM-killing the
host.

Not attempted: that is a whole session s risk for one probe, and the next session
should decide with the cost in front of it rather than discover it halfway
through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:22:13 +00:00
sylph-decoder
03c70458ad re: withdraw a wrong explanation of the BGM_001 duration gap
I wrote that BGM_001 s declared 173.821 s disagreed with a decoded 167.663 s,
and explained the gap as declared covering the encoded stream including trailing
silence while decoded is where the audio stops. The port decoded it fully: the
bank yields 173.809 s of PCM. There is no disagreement -- 167.663 s is where the
music fades out, measured from the audio, and the stream continues silent to its
declared end inside that same decode.

So the declared-rate method is better than this page claimed, and is now
cross-checked on three banks against independent decodes: BGM_103 87.750 vs
87.744, BGM_102 37.487 vs 37.482, BGM_001 173.821 vs 173.809 -- agreement 5 to
12 ms.

The conclusion survives unchanged and is the useful half: trust it for lengths,
not for musical boundaries. A declared length includes whatever silence the
encode carries, so it is not a loop point.

Also records in METHOD a defect shape the port hit three times in one pipeline,
each invisible to every check except a level: normalising by how many inputs
there are rather than how many carry signal. A silent chunk in a voice sum, a
silent channel in a mono fold, and a silent sub-wave -- the 10240-byte bank
header wrapped to 10300 B -- counted as a third stem in a music sum, which put
every real stem at 1/3 instead of 1/2 and cost 3.52 dB on all menu music for two
iterations. This corpus s census said two waves and the exporter s divisor said
three; the count that disagrees with a census is the one that is wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:20:49 +00:00
sylph-decoder
68b8f8db7f re: take 2 is also unusable -- the sink is starved, 39.3 percent digital silence
The port could not find the movie bed or the cutscene voice in take 2 either,
this time with a correlator they had rebuilt and calibrated in both directions
after retracting the first one. Their negative stands. They named two readings:
the capture path is still losing the guest s mix, or the guest is not emitting
these sources -- and flagged the second as landing on them hard, because if the
game never plays the .wmv s WMA track the port s intro audio has been wrong
since P4.

It is the first, and take 2 says so on its face:

  digital silence on all six channels   6557892 / 16680453 = 39.3 percent
  non-silent runs   10595, median 13.60 ms, longest 1.19 s
  silent runs       10596, median 3.94 ms
  burst+gap period  about 17.5 ms, 57 Hz, duty cycle 60.7 percent

The recording is chopped into 13 ms fragments separated by 4 ms holes, ten
thousand times over -- a starved sink, PulseAudio filling underruns with silence.
That destroys envelope correlation by construction, since the envelope is
dominated by a 57 Hz chop unrelated to the content. The file s strongest
periodicity is 5.2 s rather than BGM_102 s 37.487 s loop; the estimator was
controlled first, recovering a synthetic 37.487 s loop as 37.480 and scoring
non-repeating noise at 0.019.

So the port s alarming hypothesis is NOT supported. Nothing here says the game
fails to play the movie s audio; it says this capture cannot answer either way.

A monitor sink cannot fix it: parec reads a monitor that advances at wall clock
and substitutes silence, so every moment the emulator runs below real time is a
hole and the timebase is warped non-uniformly. The route that would work is an
internal tap at SDLAudioDriver::SubmitFrame, which sees every frame the guest
produces in guest order with no wall clock -- same shape as xma_param_probe,
additive and default-off. Not attempted this iteration.

Also corrects a provenance number I got wrong: I told the port take 2 was 253.3 s
when the shared file is 318.5 and the full recording 349. I read ffprobe while
the recorder was still writing and copied the file before it finished, so the
shared artefact is itself a truncation. Corrected provenance: movie 10-251, title
262-318, back to movie at 329 -- meaning the file includes the title screen,
contrary to what I told them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:19:46 +00:00
sylph-decoder
c479d59f52 handoff: deliver the BGM_102 identification and the header-derived durations
States the identification, the caution that it does NOT establish which screen
BGM_102 belongs to, and the one thing the port can use today: BGM durations from
the corrected XMA1 PsuedoBytesPerSec with no decoder. Flags that those durations
include trailing silence -- BGM_001 reads 173.821 declared against 167.663
decoded, a gap matching the 6.15 s of silence this page already records -- so a
menu loop point must use the decoded figure.

Also records that a refutation attempt on this page s own BGM_103 wave sizes
failed: both match the disc exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:16:34 +00:00
sylph-decoder
8340cc9b00 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
2026-08-29 16:16:13 +00:00
sylph-decoder
a4c05bb96b re: the capture recipe that passes, and a five-stream observation it turned up
Take 2 of the ADV audio capture verified with the port s independent
tools/port/check-capture -- six distinct channel MD5s, PASS -- run before
sharing and deliberately using their tool rather than the hand that made the
file.

Records the recipe: sink channel_map set equal to Canary s own stream map and
the same map passed to parec, so PulseAudio does no remapping; both of
run-canary s mutes off; recorder started before the emulator so WAV t=0 precedes
process launch; and a screenshot every ~11 s keyed to the recording s own clock.

That last pair is what makes it self-checking, and both were the port s asks.
Classified against the committed references, this run reads movie/other for
t=10..251 and then title_noplate at t=262 (r=+0.998) and title_plate at 277/289
-- so the 253 s of audio sits wholly inside the movie with the title arriving
just after. A miss is now diagnosable instead of ambiguous, which is the whole
difference from take 1.

Still the full mix: movie WMA bed plus voice, nothing at this boundary separates
them.

Also records something unexplained that the run turned up: the probe logged FIVE
distinct XMA byte_size values, not three -- ADV s 1294336 / 1118208 / 1171456
plus 1150976 and 1269760. The extra pair belongs to some other cue, is not
BGM_103 s two waves, and a pair is the shape bgm-two-stems documents for music
banks. Untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:10:28 +00:00
sylph-decoder
cfcc6d90a2 re: a per-channel level check is blind to remap corruption, by construction
The port made this point while building a checker for the channel-map trap, and
it refutes a sentence in my own write-up.

In the known-bad control all six channels report a peak of -18.063656 dB,
identical to six decimals, while the file contains three duplicate pairs. Equal
tone amplitudes make the peak table uniform however the channels are permuted or
duplicated; on real content the peaks simply differ from one another, which looks
equally healthy. The table is uninformative either way.

So "the WAV has plausible per-channel levels" was not weak evidence that a
capture was sound, it was none, and this page implied otherwise. The per-channel
peak table is the natural thing to eyeball after a capture and it cannot see this
failure at all. What detects it is hashing each channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:08:10 +00:00
sylph-decoder
7c16bec4cc re: a 6-channel capture scrambles and duplicates channels unless the maps match
The ADV audio capture I shared with the port is withdrawn as evidence. They
could not match it against anything -- the movie bed, any of the three voice
streams, BGM_103, S00A -- with best-vs-runner-up margins of 0.001 to 0.016
everywhere, and they controlled that three ways before saying so. They also
noticed capture channels 3 and 6 were byte-identical.

That duplicate pair reproduces without the emulator, and it is my capture chain.
Six channels each carrying a different tone, played to the null sink and
recorded from its monitor with the same parec invocation:

  sink map NOT matching the client (the original setup)
    expected  400  800  200 1600 3200 6400
    captured  400 3200  200  800  800  200     ch2 == ch5 byte-identical

  sink map made identical to Canary s stream map, and passed to parec too
    captured  400  800  200 1600 3200 6400     no duplicates -- CONTROL PASSED

PulseAudio remaps when the maps differ, and a 6-channel remap silently drops
channels and duplicates others. No error, no warning; the WAV has the right
length, channel count and plausible per-channel levels.

Withdrawn with it: "all six channels carry signal", and the observation that
non-zero surround and LFE weakly supported the 5.1 reading of a voice cue s
three streams. The port said a duplicated channel is not an independent one and
they were right before this control existed.

Unaffected: the three-XMA-context concurrency result, which is read from the
emulator s own log rather than the audio path, on two independent boots.

The control needed no emulator, no disc and thirty seconds. It was not run, an
artefact was published, and the person who found the defect was the one who
could not see the instrument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 16:04:52 +00:00
sylph-decoder
68aa19283d notes: run-canary is silent TWICE over, and the 6-channel stream is a red herring
Completes an entry committed an hour ago that was incomplete, which is worse
than absent because it looked authoritative. Fixing SDL_AUDIODRIVER alone still
records silence: run-canary also passes --mute=true on its own command line
(line 98). With the driver fixed and the mute left alone, Canary attaches a
healthy 6-channel stream, holds it at 100 percent volume, reports Corked: no,
and emits nothing. Both layers have to go, and "$@" is last so --mute=false on
the caller s side wins.

Also records that parec defaults to stereo/44.1 kHz and will resample a
6-channel monitor without saying so -- the first successful-looking capture came
back 2ch 44100 from a 6ch sink.

And a red herring I nearly published as a finding. pactl shows Canary s stream
as float32le 6ch 48000Hz with a full 5.1 channel map, which reads as the guest
requesting 5.1 and would have been strong support for the hypothesis that a
voice cue s three streams are 5.1 channel pairs. It is not evidence about the
game at all: AudioDriver::kFrameChannelsDefault is a hardcoded 6, and the code
path actually used, SDLAudioSystem::CreateDriver(index, semaphore, &driver),
constructs SDLAudioDriver(semaphore) taking every default. The format is
Xenia s; only the content of those six channels is the guest s.

That is the same failure this corpus recorded in METHOD earlier today -- the
specific observation and the general rule reading identically -- caught this
time before it was written down rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:56:24 +00:00
sylph-decoder
cb612a82ff notes: run-canary is silent by default, which records perfect silence
Both of these cost time in one session and both are the kind that look like
success.

run-canary line 82 is SDL_AUDIODRIVER=${SDL_AUDIODRIVER:-dummy}, and its own
header explains why: --apu=nop stalls the guest in the intro movie, so the SDL
driver against a dummy device is what lets the title advance. But the comment s
premise -- "there is no PulseAudio here" -- stopped being true when
tools/audio-capture landed, since that starts a daemon on demand. So a capture
through the null sink records pure silence, of the right length, behind a run
that looks perfectly healthy. The override is
PULSE_SINK=cap SDL_AUDIODRIVER=pulseaudio run-canary, and the live check is
pactl list sink-inputs: empty means Canary never attached and the sink sits at
IDLE. audio-capture s own -inf peak warning is the backstop, but it only fires
after the whole run.

Separately, pkill -f and pgrep -f match the caller s OWN command line. Hit twice
here: pkill -9 -f adv_audio_cap.sh killed the shell running it, and an
until ! pgrep -f "probe.py --run" loop never exited because the loop s own
command line contained the pattern -- which looks exactly like the job hanging.
Kill by process name with ps -o pid= -C instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:52:05 +00:00
sylph-decoder
4d2482816d handoff: deliver the concurrent-streams refutation to the page the port reads
The finding landed in docs/re/ in the previous commit; an answer not reachable
from HANDOFF is not delivered. States plainly that "take one stream" was mine,
that the port implemented it, and that it is withdrawn -- together with the
caution that summing is not thereby right, because an equal-gain 1/n sum of
channel pairs is not a downmix and the port s measured 6.02 dB loss was real.
Neither rule is established, so the manifest should say the value is authored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:49:32 +00:00
sylph-decoder
0e03e7496e re: the three streams of a voice cue are decoded CONCURRENTLY, not alternatives
Refutes a framing of mine that two documents and the port s exporter were built
on, so it is a new page rather than an edit.

voice-region-leading-chunk.md read a long cue s three streams as three
presentations of one take, and from that came the instruction "take one stream,
do not sum", which the port implemented. The open question was which
presentation the game plays.

It has no answer. Booted with --xma_param_probe=true, the cvar whose own comment
says it is keyed to reveal which sub-wave of a movie s .slb the game actually
decodes. The guest opens three XMA contexts and decodes all three concurrently:

  ctx=0  packets=632  byte_size=1294336  ch=2  48000   ADV stream 1
  ctx=1  packets=546  byte_size=1118208  ch=2  48000   ADV stream 2
  ctx=2  packets=572  byte_size=1171456  ch=2  48000   ADV stream 3

Byte-exact against the three streams payloads taken independently off the disc
(RIFF size minus 60). Only these three contexts appear in the run.

So a consumer that picks one discards two thirds of what the game mixes. Both
"three presentations of one take" and "take one stream" are withdrawn -- and the
previous behaviour is not thereby right either, because an equal-gain 1/n sum of
channel pairs is not a downmix and the port s measured 6.02 dB loss was real.
Neither rule is established; a consumer is authoring.

Three concurrent stereo streams is six channels and N stereo streams is how XMA
carries multichannel on the 360, which would also explain the 1-or-3-never-2
span census. Recorded as a hypothesis, not a result: all three fmt chunks
declare ChannelMask 0x0002 identically, which is not what distinct channel roles
should look like.

Everything byte-level survives: the leading chunk being stream 1 clipped by our
own guard, the 70 + 8 + 17 decomposition, the bank-header discriminator.

Reach: one cue, one boot. That 28 cues are 3-stream is decoded; that all three
decode concurrently is measured on ADV alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:48:35 +00:00
sylph-decoder
aad050cb4a formats: XMA1 is not a WAVEFORMATEX -- audio info was reading three wrong fields
parse_riff_wave read every fmt chunk as a WAVEFORMATEX. XMA1 (tag 0x0165) is
not one, so audio info reported the disc s movie voices as 16 channels,
4310 Hz, 2-bit: 16 is wBitsPerSample read as a channel count and 4310 is
wEncodeOptions (0x10d6) read as a sample rate. This misled me earlier in the
session and I recorded it as a limitation before finding the cause.

XMA1 carries XMAWAVEFORMAT followed by one XMASTREAMFORMAT per stream. The
reader now branches on the tag and reads bits at +2, PsuedoBytesPerSec at +12,
SampleRate at +16 and Channels at +29. The same three files now report 2
channels, 48000 Hz, 16-bit.

The consequence worth having: this crate has no XMA decoder, and
data_bytes / PsuedoBytesPerSec is the only route to a duration. Checked against
durations decoded independently by the port:

  ADV presentation 1   137.34 s declared   137.324 s decoded   +0.012 percent
  ADV presentation 2   137.33 s declared   137.324 s decoded   +0.004 percent
  S00A presentation 1   93.71 s declared    93.694 s decoded   +0.017 percent

So the corpus can now get XMA1 durations off the disc without a decoder, which
is a capability I had written down as absent. It is a declared rate rather than
a measurement of the samples, and the CLI labels it as such.

Regression test pins the real on-disc header bytes and asserts the duration
against the independently decoded 137.324 s. 115 lib tests and 3 media disc
tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:42:52 +00:00
sylph-decoder
877c028f54 method: the specific observation and the general rule read identically
Five corrections across two agents in two days share one shape, and it is worth
naming as a family rather than fixing one at a time. None was carelessness about
the measurement -- every underlying observation was true of the asset actually
looked at. The failure is reaching for the general form in the same breath as the
specific one, where the two are indistinguishable on the page and the general one
is what the next reader uses.

Three were the port s and two were mine, and the entry names both sides:

  the two chunks are two stems of one performance -- true of a music bank,
    written as a fact about voice, where one of the two is digital silence
  the extra bytes are a duplicated channel, not fidelity -- true of ADV, and the
    size ratio it implies runs 0.0778 to 2.9163 across the disc
  everything the sequencer paces off rest.t is late -- true of the title, and
    false of the screens actually checked
  a three-stream cue is a movie cue -- mine, and BIRD_224 is neither
  take the highest-rate, highest-gain stream -- mine, and on ADV those two
    criteria select different streams

The counter is the same every time: run the census before writing the rule.
Where the census cannot be run, write the specific sentence and say it is
specific.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:40:11 +00:00
sylph-decoder
7a0e18889f re: the dual-mono explanation for the extra stream bytes does not generalise
The port chose a voice presentation on the argument that ADV chunk 1 is
mono-in-stereo and chunk 2 is dual-mono, so chunk 2 s extra bytes encode a
duplicated channel rather than fidelity -- which would explain its higher
declared PsuedoBytesPerSec without appealing to encode quality.

Their ADV channel measurement stands. The generalisation does not. If stream 3
were systematically the same take with its channel duplicated, its size ratio to
stream 2 would be tight across the 28 three-stream cues. Measured:

  min 0.0778 (S00A, the silent one)
  median 1.2565
  max 2.9163 (S06A)
  sd 0.5057
  within 15 percent of 1.0: 12 of 28

A 37x spread is not a duplicated channel, and the declared rates scatter with
them -- S06A is 5661 against 16513 B/s. Whatever distinguishes the three streams
varies per cue rather than being a fixed channel-configuration triple.

This does not touch the port s decision, which is to take the loudest
presentation: that is a per-asset content measurement, not a structural rule, so
a scattering ratio cannot undermine it. It touches the explanation, which should
not harden into a fact about the format.

Two curiosities recorded: S12B s three streams are byte-size identical at 14396
each, and BIRD_224 is 3-stream while being a non-movie cue, so the shape is not
exclusive to cutscenes.

Also narrows the settle-time page s own generalisation. The port measured its
boot the way this corpus measured the game and found the sequencer NOT late --
its 0.6 s discrepancy was arrival-to-arrival timestamps compared against visible
spans, the plate-delay trap in a second place. So what is supported is that
rest.t is the wrong landmark for the TITLE, not that everything paced off it is
late. And the offered re-take of the one-run menu figures is recorded as
declined, with the reason, rather than left looking unfinished.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:37:46 +00:00
sylph-decoder
4bd4779f8b re: settle_time measured -- the title is drawn at 2 s, not at rest.t s 4.18
The port s boot sequencer paces every screen off rest.t, which is the last hold
keyframe rather than when a screen arrives. Measured on one cold boot: the
container had no Xenia storage root at all, so this is a fresh profile with no
shader cache, the slowest case.

  title build-in (first ink -> art fully drawn)   0.23 s
  title settled -> PRESS A plate on              2.247 s   (disc declares 120 units)
  plate pulse period                            ~2.37 s
  main menu build-in                             0.531 s
  B -> title                                     0.482 s
  A -> menu                                      3.763 s   DO NOT AUTHOR, see below

The title s rest.t is 251 units = 4.183 s and its art is finished at about 2 s,
so a sequencer pacing off rest.t holds it roughly twice as long as the game does.

Instrument controlled before the run: 9/9 on the content classifier including
the movie-frame and difficulty-screen negatives, 4/4 on the plate detector; the
run sampled 7.99 fps against a requested 8 with an independent one-shot grab
cross-checking every 20 s.

Records a refutation attempt of mine that FAILED. The probe s own marks gave a
plate delay of 3.203 s against the corpus s 2.13 s, which on a cold-cache boot
looked like a real effect. It was the instrument: the plate pulse period is an
internal clock for presentation rate and measures 2.369 s here against the
corpus s 2.3, so the run is not slowed, and re-measuring from content gives
2.247 s. The probe s title_static mark fires during the crossfade out of the
attract movie, before the wordmark has drawn -- glyph was still 0 when it fired.

Also a third independent reproduction of the A-path load stall: 13 frames,
1.53 s, surface mean 26.631 against the earlier 14/1.53 and 12/1.39 at 26.626.
This boot had no shader cache, so it is not a warm-cache artefact. Noted that
the earlier pair agreed to six decimals and mine agrees to three.

Reach: one run. The menu build-in and B->title rest on it alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:21:49 +00:00
sylph-decoder
82281df804 method: the plate glyph counter false-positives on the attract movie by 13x
Found while controlling the timing probe before a settle_time run, not by
reasoning about it.

title_timing_probe.py s plate detector thresholds a green-glyph pixel count at
400, and its control checks two committed movie frames that both score 0. A real
boot disagrees: in one 100 s attract window, 17 frames scored at or above 400
and the peak was 5393. The attract movie has green content in the plate region.

The probe is not wrong -- its state machine refuses to look at the glyph until
the content classifier has already labelled the frame title_noplate or
title_plate, so the false positives never reach the drive. But it is safe
because of that gate, not because the threshold discriminates, and the
distinction matters for anyone reusing glyph() on its own.

Recorded with the general form: a two-frame control over a three-and-a-half
minute movie is not a control over that movie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:17:08 +00:00
sylph-decoder
ff715938f5 re: withdraw a stream-selection recommendation that contradicted itself
The port implemented "take the highest-rate stream" and reported that on ADV it
selects the QUIETER presentation -- chunk 2, 1171516 B at -8.3 dBFS, over
chunk 1, 1118268 B at 0.0. They were right to flag it rather than accept it.

My sentence was "the highest-rate, highest-gain one is chunk 1". Those two
criteria do not select the same stream and the sentence should never have joined
them; the parenthetical named chunk 1 while the rule named chunk 2. Withdrawn.

What the header does decode, read off the bytes: the fmt chunk is a 32-byte
XMAWAVEFORMAT, little-endian, and +0x20 is a declared PsuedoBytesPerSec -- 8142
and 8530 on ADV s two presentations, agreeing with the computed rates to 0.02
percent, with 48000 Hz at +0x24. So the rate is decoded rather than inferred.

What it does not decode: wEncodeOptions (0x10d6), channel count and channel mask
are byte-identical across the presentations. Nothing in the header ranks them,
so stream selection stays an authored choice and the port must know it is
authoring. Settleable in one emulator run -- a capture of the intro with the
dialogue audible says which level the game plays -- and not yet done.

Also records that sylpheed-cli audio info is misaligned for XMA1: its "16
channels / 4310 Hz / 2-bit" is wBitsPerSample, wEncodeOptions and the channel
fields read at the wrong offsets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:13:20 +00:00
sylph-decoder
801062c933 re: a long voice cue is three presentations of one take, and our guard clips the first
Closes the last open question on the voice regions: why one cue s byte span
decodes to ~2.6x the movie s length.

The port measured, with controls including a cross-movie negative, that a
region s leading chunk is the TAIL of the full-length chunk that follows it --
r = 0.998 at a lag that puts it flush against that chunk s end, residual 16.7 dB
down over 84.5 s. They withdrew their own earlier 0.768, which came from a
search that scored best on the boundary of its own lag range.

Checked it here by an independent route that needs no decoder. If the leading
chunk is the tail of a full-length first stream, the whole leading stream should
be one complete take of chunk 1 s duration. For ADV: 504464 + 808304 = 1312768 B
at chunk 0 s byte rate of 9559.7 B/s is 137.323 s, against chunk 1 s measured
137.324 s. One millisecond over 137 seconds, from byte rates rather than from
envelope correlation.

And the byte structure settles the shape disc-wide. Counting stream starts inside
every inter-descriptor span: 258 hold exactly 1 stream, 28 hold exactly 3, and
nothing holds 2 or any other number. All 20 spans over 1.5 MB are 3-stream. The
95 movie regions decompose 70 + 8 + 17, and the 8 are independently the same 8
the first census found as bank-header-with-3-chunks.

So 359 s = 84.55 + 137.32 + 137.32: three presentations of one take, the first
clipped by resolve_movie_voice_region s own 1.5 MB guard.

Consequences recorded for the port: dropping the leading chunk is removing a
duplicate rather than truncating, so the hedge is lifted; but summing chunk 1
and chunk 2 is wrong, because they are the same take at different gain, not two
stems. Take one stream.

Also flags a coincidence I nearly built on: the 504464 B constant is structural,
not proportional -- ADV s proportional prediction lands within 8 bytes of it and
S00A s is 4305 B out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:09:40 +00:00
sylph-decoder
06a1779b19 re: the voice region s leading chunk is the movie s OWN dialogue, and a guard puts it there
My own leading hypothesis -- that the leading chunk is an in-mission VOICE_D_*
line -- is refuted, on the route the port suggested: widen the enumeration past
the 95 manifest-bound movies and the byte-span test settles it without anyone
listening.

Scanning the stream for every trailer descriptor (the (id, 0x11) pair whose id
repeats at +0x800) gives the complete cue partition, mission lines included:
287 descriptors in a 116.2 MB window, all 287 carrying an id the 4280-name
registry names. Every one of the 17 leading spans is bracketed by
desc(N-1)..desc(N) where desc(N) is that movie s OWN cue id. Zero mission lines.

The mechanism is a guard in our own resolver. resolve_movie_voice_region takes
the predecessor trailer as the region start, guards it with
end - start < 1_500_000, and falls back to the .slb TOC anchor when that fails.
Cues with a true span over the guard: 17, of which 17 are stream-opening. Cues
under it: 78, of which 0. Perfect discrimination both ways. The anchor sits a
constant 504464 B after the true predecessor trailer on all 17, which is
unexplained.

Not established, and stated as such: this does NOT mean the export truncates N
seconds. The port s decode already has ADV s region at 359 s against a 137 s
movie, so it over-covers and the byte-to-time mapping is not linear. No XMA1
decoder in this container to check.

Also withdraws a claim this page had adopted from the port -- that chunks 1 and
2 are two stems of one performance. The port refuted its own claim by decoding:
S00A chunk 2 is digital silence, ADV chunk 2 is 0.60x chunk 1 with the residual
26.8 dB down. Equal duration was a shape match and Q10 s music census should not
have been carried across to voice on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 15:03:58 +00:00
sylph-decoder
7e12a3b1f9 re: the voice-region third chunk is a different structure from the BGM one
The port hit a 2+1 chunk signature on a resolved movie-voice region and asked
whether the bank-header explanation that closed HANDOFF Q10 also covers it,
rather than assuming it. It does not, and the discriminator is mechanical.

Disc-wide over the 95 English movie-voice regions the manifest binds:

  78 open with a bank header -- bank_header_len fires, 10240 B = 5 packets
     exactly, every time. That is the BGM case.
  17 open with a leading headerless stream -- bank_header_len is None, and all
     17 have length congruent to 1392 mod 2048, the disc s own derived data
     offset. No other residue occurs.
   0 begin at a RIFF.

Counting chunks does not discriminate: 8 bank-header regions also yield three
chunks. slb.rs already predicted this in its own doc comment -- the header
signature has "zero false positives on the 7993 mid-bank windows, where the
leading region IS real" -- and a voice region is a mid-bank window by
construction.

Also tested the obvious defence of dropping the leading chunk, that it is the
predecessor cue s audio: 0 of 17 leading spans lie inside any other resolved
region, 0.0 percent on every one. The test finds overlaps where they exist (16
overlapping pairs among the regions, 60 exactly-adjacent boundaries, 73 of 78
bank-header regions starting where another ends), so the zero is not the
instrument.

Left open, with reach: the census covers movie-voice regions only, and the same
stream carries the in-mission VOICE_D_* cues, which are not enumerated -- the
leading bytes plausibly belong to one of those. Could not be settled by
listening: no XMA1 decoder in this container, and sylpheed-cli audio info
reports these chunks as 16 channels / 4310 Hz / 2-bit, which is visibly wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:56:17 +00:00
sylph-decoder
cdbf752311 re: a cross-reference kept recommending a route the cited page had killed
`menu-navigation-semantics.md` advertised "the cheap way to finish this":
read 0x828A690C as a live screen id and 0x828F38AC as the cursor, under
--gpu=null with no screenshots. `menu-state-in-memory.md` withdrew exactly that
identity ON THE SAME DAY it was published -- three back presses send the
"cursor" 36 -> 38 -> 40 -> 41, and a cursor returns when you go back. They are
monotonic counters; the cross-run agreement is the same key sequence producing
the same count.

The recommendation stood for three days after the page it cited had killed it,
and either document would have been believed on its own. Marked withdrawn where
it was recommended, with what the words ARE still good for (did the game react?)
and the consequence: a measured button->GamePart-id binding stays unfinished
because no screen enum has been located.

Also delivers Q4 to HANDOFF in the shape the port asked for -- exactly one
main-menu button opens a GP_TITLE entry (EXTRAS -> entry 6/9); the other four
leave the archive. That was measured on 2026-08-28 and was reachable only from
docs/re/, which the protocol counts as undelivered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:51:15 +00:00
sylph-decoder
202a512a12 re: a refutation attempt that failed -- EXTRAS really is a three-button screen
The port reported GP_TITLE entries 6/9 as a three-button submenu. This page and
HANDOFF call 6/9 EXTRAS and never recorded a button count, and 18 elements
looked like too many for three buttons, so I challenged it -- from the count,
without listing the elements.

Wrong. `screen info --all --build 6` shows ptbtn11/12/13 among fifteen frame,
title, background and effect layers, and entry 9 is identical. Both things are
true: 6/9 are EXTRAS (our composite correlates +0.944 whole-frame with the
committed live-extras.png) and EXTRAS is a three-button screen. The port
established the button count; the corpus did not have it.

Recorded per the adversarial duty, which is worth nothing if only the successful
challenges get written down. The retraction has gone to the port as well.

Also checked in the same pass, and it constrains Q2: entries 5 and 8 have
identical element lists and identical button placements, as do 6 and 9. The
EN/JP difference lives in the baked sprite pixels, so no layout field will ever
separate the members of either pair -- that needs the sprite images or a
capture, and the "English is the first half of the data segment" rule stays a
heuristic rather than something a field will replace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:48:46 +00:00
sylph-decoder
10a57dcdd9 handoff: say which index space a build number is in -- 10/11 meant two different screens
The port challenged HANDOFF's loading-screen row and was right. The Q2 row said
"0/1 and 10/11 are the LOADING screen"; the dated section above it says
"entries 0, 1, 12, 15". Both are true, in different index spaces, and the page
did not say which.

Verified against the bytes rather than the table:

  screen list       GP_TITLE.pak -> 12 builds, ordinals 0..11
  screen list --all GP_TITLE.pak -> 16 builds, ordinals 0..15

Only under --all does the ordinal equal the pak entry. Without it ordinal 10 is
entry 12 and ordinal 11 is entry 15. `screen info --all --build 10` shows
palogo_sqex; --build 11 shows palogo_gamearts / seta / anima; 12 and 15 show
pgloading_*. So in ENTRY space 10/11 are the publisher and developer splashes,
which is exactly the screen the wrong reading would have renamed.

It would have validated silently: the port's screen_names.json is keyed by
entry. Q2 row corrected to entry space and marked; the trap is in METHOD under
"Mechanics that have bitten", with the rule that a number leaving this
repository says "entry N", never "build N".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:47:30 +00:00
sylph-decoder
ae42f7f850 re: the paint-order tie-break costs the port zero pixels, not 24 pairs
The census bounded WHERE a wrong tie-break could show and said outright that
nobody had measured how many of those pairs change a pixel. Measured.

`compose_with_order` renders a bundle in a caller-supplied order; the new
example renders each screen twice, once derived and once with one tied pair
swapped, and diffs. Same-key elements are contiguous under a stable sort on
(key, i), so a swap paints nothing else in between.

Controlled per entry: swapping an OVERLAPPING pair with DIFFERENT keys moves
36 305 to 771 479 px (max delta 254). Where no such pair is drawn the output
says so rather than reporting an uninterpretable zero.

* EXTRAS (entries 6/9) and the main menu (5/8): 0 px. The tied ptframe pairs
  ink ~3 600 px each and share NONE of them -- the 102x132 rect overlap was an
  artefact of approximating an element as pivot x 2. Blend-independent.
* Across all 31 drawable overlapping tied pairs in GP_TITLE, the largest change
  any of them makes to any channel is 3/255.
* Withdrawn: "a wrong tie-break can be wrong by a whole layer". That rested on
  ptlogo_back2eff5 geometrically containing two other glows. Rendered, the swap
  moves 6 390 px by max delta 2. Containment is not occlusion when the container
  is a near-transparent glow, and nobody had rendered it before asserting it.

Reach: this measures our compositor's sensitivity to order, not the game's. The
zero-shared-ink results hold under any per-pixel blend; the delta<=3 figures
assume ours.

13 disc-gated ui_paint_order_disc tests and 114 lib tests pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
2026-08-29 14:47:19 +00:00
sylph-decoder
c5b815c5aa re: the paint-order tie-break costs 24 overlapping pairs, not one element
The port challenged HANDOFF's "costs one element's blend on one screen" with a
census of 105 elements sharing a layer key across 12 of 16 screens. The two
numbers count different things -- elements vs overlapping pairs -- so it is not
the contradiction it looked like, but the objection stands and the line was
wrong.

paint_order_audit already reports overlapping ties per entry, and over all 16
GP_TITLE entries: 5 use a measured order and carry no tie risk; of the 11 that
fall back to the derived order, 7 have overlapping ties, 24 pairs in total. The
Japanese title (entry 7) alone has 16, because it is the twin of the one build
whose measured order exists and has none of its own.

Overlap bounds where a wrong tie-break COULD show, not what it costs; nobody has
measured how many of the 24 change a pixel, and the port is right about that too.

The rule itself is unaffected: the layer key is still decoded and the derived
order still reproduces every measured order exactly except the title's eight
tied pairs.

Census committed at docs/re/data/paint-order-ties-gp_title.txt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:17:40 +00:00
sylph-decoder
5a0fa8f6f9 handoff: address the loading bundles by pak entry, not by is_build ordinal
The port caught this before writing a name: over the twelve bundles is_build
accepts, ordinals 10/11 are entries 12/15, while ENTRIES 10/11 are the two
splash bundles. A key written from an ordinal names the publisher wordmark as a
loading screen and still validates. Restated in entry space throughout.
2026-08-29 14:15:50 +00:00
sylph-decoder
0fd21bd744 re: the boot shows the publisher first -- the TSV that says otherwise attached late
The port flagged docs/re/data/boot-timeline-2026-08-29.tsv, whose label column
runs splash_dev before splash_pub, as a possible boot-order bug in its tree.

Three cold boots, t=0 at launch, no pad input, and the frames looked at rather
than only correlated: SQUARE ENIX 3.05-7.34 / 1.18-5.78 / 1.19-5.56 s, then a
~0.25 s black hold, then GAME ARTS/SETA/studio anima. Publisher first, 3/3.

The TSV is not wrong about any frame; its t=0 is ~7.7 s into the guest's boot,
so the publisher splash had been and gone before the stream opened. The tell is
in the file: its first twelve rows are byte-identical to four decimals -- one
held frame sampled twelve times -- and those exact numbers reappear in my run 1
at 8.42-10.94 s.

Second trap, new: ADV.wmv opens with its own SQUARE ENIX card, bloomed and below
centre, scoring 0.59-0.75 against live-splash-publisher.png. The classifier
fires splash_pub twice per boot and the second one is a movie frame. The real
splash holds perfectly still and scores 0.93-0.94.

And the dwells are DECODED, not measured: the publisher declares 240 units
(4.000 s) and the developer 195 (3.250 s), against measured 4.30/4.60/4.37 and
3.51/3.50/3.37. Measured over declared is 1.085 on average across six spans --
a 30 Hz timeline at 27.6 fps, which is the presentation rate this corpus has
measured independently three times. The port authors nothing here.

Instrument control run first: 11/11 content, 4/4 plate, the two splash
references rejecting each other at 0.035.

docs/re/boot-order-and-splash-dwell.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:15:28 +00:00
sylph-decoder
b28dfedf35 re: index and cross-references for the record layout and the loading screens 2026-08-29 14:06:10 +00:00
sylph-decoder
33a15efa3c handoff: the keyframe record layout, and the loading screens are named 2026-08-29 14:05:19 +00:00
sylph-decoder
a20d3f995d re: the DELTASABER plates are the loading screen, and the pak is packed EN-then-JP 2026-08-29 14:04:42 +00:00
sylph-decoder
07251eaa89 re: the splash timing table was already reading the corrected pairing 2026-08-29 14:04:04 +00:00
sylph-decoder
a9755170ee formats: a keyframe's time comes before its pose, and none of them was missing
The placement region is `frames` records of `{u32 time; 36-byte pose}` after an
8-byte header, so the time word PRECEDES the pose it belongs to. Our parser's
40-byte window opened at the pose, four bytes into the record, and then read the
word at its `+36` as that pose's time -- which is the NEXT pose's. Every pose
field was right; only the time association slipped by one.

Two things the corpus has carried for weeks are that off-by-one and nothing
else: "a group's data stops 4 bytes short of its final block's time slot", and
"the last keyframe carries no time". The group is not short (8 + frames*40 is
exact) and no time is missing -- the first pose's time is the lead-in word at
`header + 8` that `parse_placements` skipped without asking what it was.

Disc-wide, 33 archives, 13 991 groups, each test with a control:

  A  lead-in prepended to the shifted times is non-decreasing  13991/13991
  B  a non-zero lead-in is strictly below the next time         5058/5058
     control (another group's lead-in, same bundle)            70.9%
  C  multi-segment alpha ramp at a constant rate, corrected    857/1540
     the same, under the old reading                             0/1042

C is the one that cannot be argued with: interpolation between keyframes is
linear, and under the old reading not one multi-keyframe ramp on the disc comes
out at a constant rate.

Adoption is free on every static composite, which is what the corpus previously
declined it over. `SYLPHEED_KF_TIME_SHIFT=1` moved GP_TITLE build 7 by 13.1% of
its pixels because it left pose 0 untimed; with the lead-in restored, all 12
GP_TITLE builds render byte-identically, and across 217 builds in six archives
only two elements pick a different rest pose -- both times between two poses
that are equally invisible.

`SYLPHEED_KF_TIME_SHIFT` is gone; `SYLPHEED_KF_TIME_LEGACY=1` restores the old
reading for A/B work.

ui_header_time_disc needed one line: 546 bundles whose every group is a single
static pose now report max_time = 0 where they previously reported no time at
all. Excluding them, the result it guards strengthened -- the bound holds over
2 859 bundles instead of 2 313, still with zero violations.

Not established: the executable's own parser. Reach is written down.

docs/re/ui-keyframe-record-layout.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nsxw1A9JseUw99Yw1ZRQzY
2026-08-29 14:01:39 +00:00
sylph-decoder
7e57601c15 Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 13:24:03 +00:00
MechaCat02
f1b87e47b6 decoder: tell it about the reference assets, and that the DB can be wrong
Some checks failed
CI / Native — ubuntu-latest (push) Failing after 8m7s
CI / WASM — Web (push) Failing after 6m38s
CI / Formatting (push) Failing after 1m37s
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
The mounts landed but the agent could not learn of them: I documented them in
CONTAINER-NOTES.md, which the decoder's prompt does not list, and then restarted
the container -- so a fresh session with no memory of the exchange had a 586 MB
database and a decompressed image sitting unmentioned in its filesystem.

Now in the PROMPT itself, not only in a document, because the prompt is the one
thing a new session is guaranteed to read. CONTAINER-NOTES.md is also added to
its reading list.

And the caveat that matters more than the asset. The .pe is PRIMARY -- the bytes
the console executed. The database is somebody's ANALYSIS of them, produced by a
disassembler that had to guess, and it is wrong in the ways disassemblers are
wrong: misdecoded mnemonics where data was read as code, function boundaries
short or long or merged or split, coverage missing entirely for code reached only
by indirect dispatch, and names that are derived rather than symbols.

So a finding resting on a database row is not established until the bytes agree:
read the same address out of the .pe and check. Where they disagree the image
wins, and the disagreement is itself worth recording, because it tells the next
reader which parts of the database to distrust.

A fast index into 9.2 MB of machine code, not a source of truth.
2026-08-29 15:23:47 +02:00
sylph-decoder
cef08bd351 Merge remote-tracking branch 'origin/main' into auto/no-disc-and-menu-captures 2026-08-29 13:18:09 +00:00
MechaCat02
72b10e7d03 decoder: mount the disassembly DB and the flat VA image
Some checks failed
CI / WASM — Web (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Native — macos-latest (push) Has been cancelled
CI / Native — windows-latest (push) Has been cancelled
CI / Native — ubuntu-latest (push) Has been cancelled
The decoder had neither, and reported the gap precisely: four scripts in this
repo READ /work/xenia-rs/sylpheed.db and nothing produces it, so the whole static
PPC route was consumers with the producer missing.

Both exist on the host and are now mounted read-only: the 586 MB database (25 481
functions, 851 classes with RTTI, EH tables, imports, 1.8M indirect-dispatch
candidates) and the decompressed image.

The image is the more useful of the two. It is a FLAT VA DUMP -- file offset =
VA - 0x82000000 -- so reading a known address needs no XEX decrypt, no LZX, and
no booted emulator. The decoder had independently recovered the same bytes by
dumping /dev/shm/xenia_memory_* and validating against the GamePart table, which
is good work and a sound method, but it noted itself that needing a running
emulator is a bad dependency for something the entire static corpus rests on. It
does not need one.

Also recorded that an earlier claim the .pe was STALE was tested and refuted, so
nobody re-litigates it, and that instructions.raw is an INT rather than hex.

Written down as reference material, explicitly NOT a deliverable: they are
read-only, they come from outside the repository, and a fresh checkout elsewhere
has neither. Reimplementing the producer belongs in sylpheed-formats, and until
it exists every static finding rests on an artefact this project cannot rebuild.
2026-08-29 15:17:52 +02:00
sylph-decoder
e07c1de1b5 re: I cannot measure this emulator's clock -- and that answers the port's 2%
The port put two of my pages against each other: settle->plate 2.135 s and one
focus-ring revolution 2.177 s, both a declared 120 units during a static hold,
2% apart against a 6 ms run-to-run agreement. Fair challenge.

The resolution is that the question assumes a stable wall clock. Same interval,
same container, same day: 2.138, 2.132, and 2.549 s -- a 19% swing, caused by
adding --log_ui_draws=true. The 2% is a fifth of that. The two pages were never
in conflict about the game; they are three readings of one declared quantity
through a clock that moves. What settles the quantity is the disc.

Wall clock cannot separate the hypotheses, so I tried to measure frames instead.
Both instruments are recorded as failures rather than published as numbers:

  * Canary's own [UI-CAP] counter -- the one that produced the corpus's 28.5 fps
    -- costs a third of the frame rate. 300 frames in 16.567 s = 18.11 fps on a
    screen that gives ~28 without it. That reclassifies 28.5 as a load-dependent
    lower bound; it does not overturn it.
  * A distinct-frame counter over the spinning ring FAILED its decisive control:
    15.88 fps against the game's own 17.59 in the same window, 10% low, so the
    ring does not change on every presented frame. Its static control also read
    2.63 instead of ~0. Dead, not tuneable, per METHOD.md.

The rule that follows, and it applies to everything I hand the port: a measured
interval landing near a round number of declared units almost certainly IS that
number of units. Ship the units.

Also recovered here, because the same question needed it: the static PPC route.
Four tools open /work/xenia-rs/sylpheed.db and nothing in this repository builds
it -- no disassembler, no PPC decoder, and default.xex is encrypted (zero
plaintext "GamePart"). Xenia decompresses the image at load, so dump_image.py
reads it out of guest memory and validates it against the corpus's own landmarks:
the 29-entry GamePart id table at 0x820A1630 and the Xbox 360 D3D runtime
strings. String search and table dumps work again; instruction-level work does
not, and the present interval I wanted is an immediate, not a string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 13:16:02 +00:00