diff --git a/docker/bin/push-work b/docker/bin/push-work index 5a1d13b..d072ff8 100755 --- a/docker/bin/push-work +++ b/docker/bin/push-work @@ -16,7 +16,7 @@ # `sylph-agent`). They are never printed, never logged, and never passed on a # command line. # -# push-work push the current branch +# push-work push the current branch, and any annotated tags on it # push-work --dry-run say what it would do set -euo pipefail @@ -72,5 +72,5 @@ fi # --force-with-lease is deliberately NOT offered. If this is rejected as # non-fast-forward, someone else moved the branch: fetch and merge, do not # overwrite. -git -c "credential.helper=$CRED_HELPER" push --set-upstream origin "$branch" +git -c "credential.helper=$CRED_HELPER" push --follow-tags --set-upstream origin "$branch" echo "push-work: pushed $branch" diff --git a/docs/AUDIO-VERIFICATION.md b/docs/AUDIO-VERIFICATION.md new file mode 100644 index 0000000..41c80c9 --- /dev/null +++ b/docs/AUDIO-VERIFICATION.md @@ -0,0 +1,110 @@ +# Verifying audio without an audio device + +Neither container has a sound card, so "does it actually play?" cannot be +answered by listening. It can be answered by measurement, and the two things +usually meant by that question need different measurements. + +**Separate them before reaching for a tool:** + +| question | needs Godot? | needs a device? | +|---|---|---| +| Is the transcoded file faithful to the source? | no | no | +| Does Godot actually route it to an output? | yes | no | +| What does the *game* play on a menu move? | no (Canary) | a virtual one | + +## 1. Transcode fidelity — file against file + +This is the question P4 actually raised, and it needs neither an engine nor a +device. Decode both, subtract, and measure what is left. + +```bash +# Source, for a reference level +ffmpeg -hide_banner -t 25 -i ADV.wmv \ + -af "aformat=channel_layouts=stereo,astats=measure_perchannel=none" -f null - 2>&1 \ + | grep "RMS level" + +# The difference signal: source minus transcode +ffmpeg -hide_banner -t 25 -i ADV.wmv -t 25 -i ADV.ogv -filter_complex \ + "[0:a]aformat=channel_layouts=stereo[a];\ + [1:a]aformat=channel_layouts=stereo,volume=-1[b];\ + [a][b]amix=inputs=2:normalize=0,astats=measure_perchannel=none" -f null - 2>&1 \ + | grep "RMS level" +``` + +A faithful transcode puts the difference **40 dB or more below** the source. + +### Three ways this measurement lies + +Run it wrong and it reports a disaster that is not there. All three of these +were hit on the first attempt: + +* **Alignment.** A one-sample offset makes the difference nearly as loud as the + source. Cross-correlate and compensate *before* subtracting, or the number is + meaningless. A first run gave source −25.3 dB against difference −34.2 dB — + only 9 dB down, which looks catastrophic and proves nothing. +* **Channel layout.** The source and the transcode do not have the same channel + count. You are not comparing like with like unless both sides are downmixed + the same way, and `astats` will give you a confident number regardless. See + [`movie-audio-channels`][mac] for which profile a given movie is in — that is + a disc fact and lives in the RE corpus, not here. +* **A file still being written.** `ffprobe` reported the `.ogv` as 33 s against + the source's 137 s — apparent catastrophic truncation, actually a transcode in + progress. Check `mtime` and packet count before believing a duration, and + write to a temp name and rename on completion so a reader cannot see a partial + file at all. + +⚠️ **The downmix is an unrecorded decision, and it is not ours to make quietly.** +Nothing in the manifest says a fold happened or on what weighting; it is whatever +ffmpeg defaulted to, and that default can change between versions. Centre-channel +dialogue folds into L/R, so this changes how speech sits against music — an +aesthetic judgement, not a container detail. Pin it explicitly and record it, +exactly as MISSION §6 requires of the transcode command itself. + +[mac]: https://git.mc02.dev/fabi/Syplheed-Reborn/src/branch/main/docs/re/structures/movie-audio-channels.md + +## 2. Engine routing — Godot writes a WAV instead of a device + +Godot does not need a sound card to produce audio you can inspect. Put an +`AudioEffectRecord` on the **Master** bus and it captures the mixed output from +inside a headless run: + +```gdscript +var bus := AudioServer.get_bus_index("Master") +var rec := AudioEffectRecord.new() +AudioServer.add_bus_effect(bus, rec) +rec.set_recording_active(true) +# ... play the scene ... +rec.set_recording_active(false) +rec.get_recording().save_to_wav("user://master.wav") +``` + +Then feed that WAV through §1 against the source. That closes the loop: it +proves the asset is right **and** that the engine reached it, which no amount of +file comparison can show on its own. + +Confirm the dummy driver is what is actually in use rather than assuming it — +`AudioServer.get_driver_name()` — and say so in the write-up, because "recorded +under a dummy driver" is a weaker claim than "heard", and the difference matters. + +## 3. A virtual device, when something insists on a real one + +For anything that opens a device rather than a bus — the emulator, most +obviously — a PulseAudio **null sink** is a real device that records to a file: + +```bash +pactl load-module module-null-sink sink_name=cap sink_properties=device.description=cap +PULSE_SINK=cap +parec -d cap.monitor --file-format=wav /tmp/captured.wav +``` + +This is the route to capturing what the *game* plays — the menu move and confirm +cues behind HANDOFF Q8 — rather than what we think it should play. It needs +`pulseaudio-utils` in the image, so it is a rebuild, not something to reach for +mid-iteration. + +## What none of this establishes + +That it *sounds right*. Every method here shows correspondence to a source, not +that the source is the audio the game plays at that moment, and not that levels +are sane in a mix. A ten-second human listen still answers something no +measurement above does — so when a result rests on one of these, say which one. diff --git a/docs/MISSION.md b/docs/MISSION.md index f5ed619..3333b63 100644 --- a/docs/MISSION.md +++ b/docs/MISSION.md @@ -43,10 +43,21 @@ XMA, no WMV. If Godot cannot read something, the exporter's job is to emit it differently — not to bridge the gap at runtime. * **No GDExtension. No Rust in `port/`.** -* The decoders come from `sylpheed-formats`, **pinned by revision**. Do not vendor - them, do not reimplement them, and do not float the pin — a decoder change - landing mid-milestone is exactly the confusion this pin prevents. -* Bump the pin deliberately, as its own commit, saying what you wanted from it. +* The decoders come from `sylpheed-formats`, **pinned by TAG**: + `sylpheed-formats = { git = "...", tag = "formats-pin-2026-08-29" }`. + + Pin a tag, never a bare sha. A sha reachable only from an `auto/*` branch is + orphaned when that branch is deleted or — worse — **squash-merged**, because + squash creates *new* commits: `main` looks like it contains the work while the + pin becomes unreachable and this project stops building for a fresh checkout. + A tag is a permanent ref, it says what it is in `Cargo.toml`, and it fails + loudly at *fetch* rather than silently at build. +* **Do not float the pin** to a branch. It would not do what it sounds like: + Cargo resolves a git dependency once and writes the sha into `Cargo.lock`, so + floating gives you staleness you cannot see instead of staleness you can read. +* Bump deliberately, as its own commit, saying what you wanted from the new + state. The RE agent tags when it lands something you need and tells you over + the message channel — that is how you stay current without floating. **In particular, do not reimplement media assembly.** `sylpheed_formats::media` already handles the cases where one playable thing is not one archive entry: a