diff --git a/docs/port/AUDIO-VERIFICATION.md b/docs/port/AUDIO-VERIFICATION.md index 2de5861e..03c99215 100644 --- a/docs/port/AUDIO-VERIFICATION.md +++ b/docs/port/AUDIO-VERIFICATION.md @@ -116,6 +116,71 @@ silent**, because silence is the failure that looks like success: a WAV of exactly the right duration, full of zeroes, because the application opened a different sink. A duration check alone would pass it. +## 5. A multichannel capture must pass a provenance check BEFORE it is analysed + +`tools/port/check-capture FILE.wav` — run it first, every time. + +⚠️ **This section exists because a capture of the game's own 6-channel output was +analysed at length and the file was corrupt.** It got three controls, a +drift test and a written-up negative, and every one of those was sound; none of +them could see that channels were missing, because the corruption was upstream of +everything they tested. + +**PulseAudio was remapping between two mismatched channel maps, and a 6-channel +remap silently drops and duplicates.** The Decoder proved it with a control that +needs no emulator and no disc — six channels each carrying a different tone, +through the same sink and the same `parec` invocation +(`docs/re/audio-capture-channel-map-trap.md`): + +| ch | played | recorded | +|---|---|---| +| 0 | 400 | 400 | +| 1 | 800 | **3200** | +| 2 | 200 | 200 | +| 3 | 1600 | **800** | +| 4 | 3200 | **800** | +| 5 | 6400 | **200** | + +**Two source channels were gone entirely** and two were duplicates. Setting the +sink's `channel_map` to the guest's own (`FL,FR,FC,LFE,RL,RR`) and passing the +same map to `parec` returns all six. + +### The signature is an exact duplicate pair, and only a hash finds it + +Duration is right. Channel count is right. `Corked: no`. There is no error +anywhere, and the **per-channel levels look entirely reasonable** — which is the +whole difficulty. In the tool's own known-bad control, all six channels report a +peak of **−18.063656 dB, identical to six decimals, while containing three +duplicate pairs.** A level check cannot see this. Hashing each channel can. + +Two channels of a real surround mix are never byte-identical over tens of +seconds. On the corrupt game capture the tool reports: + +``` +ch2 peak -4.466272 ba497de78217c438a3e430c5ef6b951b +ch5 peak -4.466272 ba497de78217c438a3e430c5ef6b951b +🔴 ch2 and ch5 are BYTE-IDENTICAL +``` + +⚠️ **It is a necessary check, not a sufficient one.** Passing says the file has no +duplicated channels. It says nothing about whether the right thing was recorded — +that is what §1's correlation against a known source is for, and a capture should +survive **both** before anything is concluded from it. + +### Two more conditions, learned the same way + +* **Start the recorder before the process you are capturing**, so `t = 0` + precedes it and the window certainly contains the moment of interest. +* **Log what was on screen, with timestamps keyed to the recording's own clock.** + A capture that matches nothing is then diagnosable rather than ambiguous; the + corrupt one could not be told apart from "recorded the wrong phase of the boot" + by any amount of analysis at this end. + +And the failure this page already warns about, in a second costume: +`run-canary` is silent **twice over** — `SDL_AUDIODRIVER=dummy` *and* +`--mute=true`. Fix only the first and Canary attaches a healthy 6-channel stream +at 100 % volume, reports `Corked: no`, and emits a 19 MB WAV of zeroes. + ## What none of this establishes That it *sounds right*. Every method here shows correspondence to a source, not diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ce38a61f..5ffc1710 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -3189,3 +3189,54 @@ Still one stream of three, still marked 🔴 in the manifest, the console line a outcome for a measurement that failed: the question is open, and it was open before. What would have been wrong is treating a 70 s recording as an oracle because it was expensive to obtain. + + +### Resolved the same day — it was the capture path, and the duplicate pair was the thread + +The Decoder found the cause and **withdrew the capture**: PulseAudio was +remapping between two mismatched channel maps, and a 6-channel remap **silently +drops and duplicates**. Its control needs no emulator and no disc — six channels, +six different tones, the same sink and the same `parec` invocation — and came +back `400 / 3200 / 200 / 800 / 800 / 200` for an input of +`400 / 800 / 200 / 1600 / 3200 / 6400`. **Two source channels were gone +entirely.** Setting the sink's `channel_map` to the guest's own returns all six. + +So the negative was right and, more usefully, **the byte-identical pair I +reported was the thread that unravelled it.** That is worth recording precisely, +because it was nearly not reported at all: it began as an idle check of two +channels whose peak *and* RMS matched to six decimals, and the only reason it +became evidence is that a coincidence at six decimals is cheaper to hash than to +explain. + +**Withdrawn with the file**, both the Decoder's: *"all six channels carry +signal"*, and the non-zero-surround observation offered as weak support for a 5.1 +guest mix. 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. + +### What the port took from it: `tools/port/check-capture` + +A capture now has to pass a provenance check before anyone analyses it, and it is +one command. It splits the file, hashes every channel and fails on any duplicate +pair. Documented in `docs/port/AUDIO-VERIFICATION.md` §5. + +**Run through its own controls, both directions**, because a checker nobody +controlled is the thing this whole incident is about: + +* six distinct tones → **PASS**; +* the remap's own output pattern → **FAIL**, naming all four duplicate pairs; +* the corrupt game capture → **FAIL** on `ch2 == ch5`. + +⚠️ **The known-bad control is the part worth reading.** All six of its channels +report a peak of **−18.063656 dB — identical to six decimals — while containing +three duplicate pairs.** A level check cannot see this failure at all. That is +why the tool hashes rather than measures, and it is why the corrupt capture's +"plausible per-channel levels" were never evidence of anything. + +The tool says so itself: it is **necessary, not sufficient.** Passing means no +channel was duplicated; it says nothing about whether the right thing was +recorded. A capture should survive both that and §1's correlation against a known +source before anything is concluded from it — and the one that was analysed here +would have failed the cheap check in thirty seconds. + +The corrupt file is withdrawn from the exchange (`share drop`), so the next agent +cannot pick it up and repeat the work. diff --git a/tools/port/check-capture b/tools/port/check-capture new file mode 100755 index 00000000..c6893c54 --- /dev/null +++ b/tools/port/check-capture @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Provenance check for a multichannel capture, BEFORE anybody analyses it. +# +# tools/port/check-capture /path/to/capture.wav +# +# WHY THIS EXISTS. A 6-channel capture of the game's own output was analysed at +# length -- three controls, a drift test, a written-up negative -- and the file +# was corrupt. PulseAudio was remapping between two mismatched channel maps, and +# a 6-channel remap SILENTLY DROPS AND DUPLICATES: right duration, right channel +# count, plausible per-channel levels, no error anywhere. Two of the six channels +# were byte-identical copies of two others and two source channels were simply +# gone. +# +# The Decoder proved it with a control that needs no emulator and no disc: six +# channels each carrying a different tone through the same sink and the same +# `parec` invocation. Channels came back 400 / 3200 / 200 / 800 / 800 / 200 for +# an input of 400 / 800 / 200 / 1600 / 3200 / 6400 -- see +# `docs/re/audio-capture-channel-map-trap.md`. Setting the sink's `channel_map` +# to the guest's own and passing the same map to `parec` returns all six. +# +# THE DETECTABLE SIGNATURE IS AN EXACT DUPLICATE PAIR. Two channels of a real +# surround mix are never byte-identical over 70 s. Levels are not enough to catch +# it -- the corrupt file's per-channel peaks looked entirely reasonable, and it +# was only equal peak AND equal RMS to six decimals that prompted a hash. +# +# This is a NECESSARY check, not a sufficient one: passing it means the capture +# has no duplicated channels, not that it recorded the right thing. +set -euo pipefail +f="${1:?usage: check-capture FILE.wav}" + +# Queried one field at a time. A combined `-show_entries` prints two values on +# ONE comma-separated line, and `read -r ch rate dur` then puts "48000,6" in +# `$ch` -- which every later arithmetic test rejects, in a script whose whole +# job is to be trusted about a file. +probe() { ffprobe -v error -select_streams a:0 -show_entries "$1" -of csv=p=0:nk=1 "$f" | head -1; } +ch=$(probe stream=channels) +rate=$(probe stream=sample_rate) +dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0:nk=1 "$f" | head -1) +printf '%s: %sch %sHz %.3fs\n' "$f" "$ch" "$rate" "$dur" + +if [ "$ch" -lt 2 ]; then echo " single channel -- nothing to compare"; exit 0; fi + +layout=5.1; [ "$ch" = 2 ] && layout=stereo +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT +map=""; for i in $(seq 0 $((ch-1))); do map="$map -map [c$i] $tmp/c$i.wav"; done +split=""; for i in $(seq 0 $((ch-1))); do split="$split[c$i]"; done +# shellcheck disable=SC2086 +ffmpeg -hide_banner -v error -y -i "$f" \ + -filter_complex "channelsplit=channel_layout=$layout$split" $map + +declare -a sums +for i in $(seq 0 $((ch-1))); do + s=$(ffmpeg -hide_banner -v error -i "$tmp/c$i.wav" -f md5 - | cut -d= -f2) + peak=$(ffmpeg -hide_banner -v info -i "$tmp/c$i.wav" -af astats -f null - 2>&1 \ + | grep -m1 "Peak level dB" | sed 's/.*: //') + sums[i]="$s" + printf ' ch%-2d peak %-12s %s\n' "$i" "$peak" "$s" +done + +dupes=0 +for i in $(seq 0 $((ch-1))); do + for j in $(seq $((i+1)) $((ch-1))); do + if [ "${sums[i]}" = "${sums[j]}" ]; then + echo " 🔴 ch$i and ch$j are BYTE-IDENTICAL" + dupes=1 + fi + done +done + +if [ "$dupes" = 1 ]; then + echo "FAIL: duplicated channels. A surround remap drops and duplicates silently;" + echo " channels are missing from this file. Do not analyse it -- fix the" + echo " sink's channel_map and re-record. See docs/port/AUDIO-VERIFICATION.md." + exit 1 +fi +echo "PASS: no duplicated channels. (Necessary, not sufficient -- this says" +echo " nothing about whether the right thing was recorded.)"