Add synchronized MPEG movie audio

This commit is contained in:
gamer147
2026-07-25 11:49:50 -04:00
parent 1828701872
commit 843b282cc4
24 changed files with 1336 additions and 558 deletions

View File

@@ -18,19 +18,19 @@ The VM and content pipeline are already mostly platform-neutral:
- Native-compatible persistence uses managed streams behind `INativeDatStore`; Godot redirects AGE's
engine-owned root to `user://SAVE` while retaining its fixed portable DAT/STH filenames.
The selected movie path now uses the project-owned FFmpeg C ABI rather than a Windows multimedia API, but only a
Windows-x64 native bundle is built and staged today. The retired-live DirectShow implementation remains in-tree
until the corpus and manual gates pass. There are also softer Windows assumptions that should be tested or
replaced before claiming portable exports.
The sole movie path now uses the project-owned FFmpeg C ABI rather than a Windows multimedia API, but only a
Windows-x64 native bundle is built and staged today. The accepted DirectShow fallback was deleted after the
corpus and audible LOGO/OP/CHAPTER gates passed. There are also softer Windows assumptions that should be tested
or replaced before claiming portable exports.
## Dependency inventory
| Area | Current dependency | Runtime impact | Portability status / future action |
|---|---|---|---|
| Retained graphics presentation | Backend-neutral `GfxState`; accepted default Godot `Sprite2D` GPU stage plus the retained software pixel oracle, using runtime `ImageTexture`, canvas transforms/materials, and no native graphics API | GPU backend caches static/color-key variants, updates dynamic surfaces, handles retained range transitions, and falls back whole-frame for the legacy host screen-transition path | Godot owns D3D/Vulkan/Metal/OpenGL selection; validate shader/blend/filter behavior per target rather than adding a platform renderer |
| AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the selected live factory over the project-owned `native/age_movie_ffmpeg` ABI; `DirectShowMovieDecoder` remains unselected pending deletion | Windows-x64 passes the complete 213-payload installed-corpus gate; other native targets and the remaining windowed CHAPTER/combat checks are pending | Run the windowed live gates, then delete DirectShow and add target-specific native builds |
| Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory, and the FFmpeg worker paces PTS against a monotonic clock with cancellation/failure completion | Concurrent/restarted uses of one asset have independent frame/completion/teardown state; `Main` remains annotated Windows because only the win-x64 bundle is available | Recheck the corrected combat lifecycle, then add Linux/macOS builds and remove the Windows annotation after DirectShow is deleted |
| Movie audio | FFmpeg detects the audio stream but the current ABI returns video frames only | MPEG movie audio remains intentionally silent | Extend the ABI with timestamped PCM and select an audio/presentation clock; separate feature slice |
| AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback | Add target-specific native builds and export packaging |
| Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state; managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates |
| Movie audio | ABI v2 returns timestamped stereo float PCM; bounded managed buffering feeds a per-playback Godot `AudioStreamGenerator` and routes native movie flags to engine buses | All 29 installed audio-bearing streams decode with signal; synchronized LOGO/OP/CHAPTER playback is audibly accepted | Treat absent, distorted, or unsynchronized audio from an audio-bearing movie as a runtime bug |
| ADV font discovery | `godot/Main.cs` probes `C:/Windows/Fonts` for Japanese fonts | Harmless fallback today, but appearance depends on host fonts | Bundle/configure a redistributable font or add platform-specific discovery |
| Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules |
| Save/profile storage | Managed `DirectoryNativeDatStore` under Godot `user://SAVE`; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails | No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target |
@@ -53,41 +53,43 @@ VM op 0x236 (non-modal) / op 0x20f (modal)
-> IHost.PlayMovieToSurface / PlayModalMovieToSurface
-> VFS-owned MoviePayload bytes
-> IMovieDecoderFactory
-> FfmpegMovieDecoder (current live selection)
-> FfmpegMovieDecoder
-> FfmpegMovieSession -> age_movie C ABI
-> DirectShowMovieDecoder (unselected; retained only through acceptance)
-> newest RGBA frame
-> retained movie surface
-> Godot compositor
-> newest due RGBA frame -> retained movie surface -> Godot compositor
-> timestamped stereo float PCM -> per-playback AudioStreamGenerator
```
Everything before and after the selected decoder is portable. The replacement decision is an in-process
FFmpeg backend behind a project-owned C ABI, not raw FFmpeg structs in Godot/C# and not a subprocess. FFmpeg
`n8.1.2-29-g703dcc25b9` is pinned by immutable release URL and SHA-256 in
`native/age_movie_ffmpeg/dependency-win64.json`; changing that pin requires rerunning the full installed-movie
gate. The shim dynamically links an LGPL build made without GPL or nonfree components and
uses only `libavformat`, `libavcodec`, `libavutil`, and `libswscale` for the video slice. `libswresample` and the
MPEG audio decoders may be packaged now, but PCM delivery remains a separate slice. Release artifacts must carry
gate. The shim dynamically links an LGPL build made without GPL or nonfree components and uses
`libavformat`, `libavcodec`, `libavutil`, `libswscale`, and `libswresample`. Release artifacts must carry
the matching FFmpeg source/configuration and notices required by FFmpeg's
[license checklist](https://ffmpeg.org/legal.html).
The boundary has two layers:
1. A small native `age_movie` ABI owns all FFmpeg objects and version-sensitive calls. It accepts a borrowed
byte span only for the duration of `open`, copies it into native-owned memory, creates a seekable custom
`AVIOContext`, probes the MPEG program stream, and returns immutable video metadata. Sequential decode returns
one top-down tightly packed RGBA8 frame plus its normalized presentation timestamp. Conversion occurs in an
byte span only for the duration of `open`, copies it once into native-owned memory, and creates independent
seekable custom `AVIOContext` views for video and audio so either stream can be pulled without duplicating the
payload or sharing a demux cursor. ABI v2 returns immutable video/audio metadata. Sequential video decode
returns one top-down tightly packed RGBA8 frame; sequential audio decode returns one interleaved stereo float32
PCM block. Both carry timestamps normalized to a common media origin. Video conversion occurs in an
FFmpeg-owned aligned frame; only exact visible row bytes are copied into the caller's tightly packed buffer.
`libswresample` converts the source channel/sample layout to the Godot generator contract.
Return statuses distinguish frame, EOF, invalid arguments, undersized output, and decoder failure; the open
call returns a bounded UTF-8 diagnostic and an opened handle retains its last decode error. Close accepts a
null handle, and managed `SafeHandle` ownership guarantees one close for each successfully opened handle. No
FFmpeg pointer crosses the ABI.
2. Managed `IMovieDecoder` owns the native handle and the paced worker. Synchronous construction provides
`StopTimeMs` before `0x236` returns. The worker uses a monotonic playback origin, decodes ahead by at most one
frame, publishes frames when their timestamps become due, and retains newest-frame-wins behavior if Godot is
late. EOF becomes completion only after the final frame's presentation interval/stop time, so surface cleanup
cannot erase the last frame immediately. Disposal interrupts waits and joins without depending on Godot's main
thread.
2. Managed `IMovieDecoder` owns the native handle and cancellable video/audio workers. Synchronous construction
provides `StopTimeMs` and audio format before `0x236` returns. Video remains at most one due frame ahead; PCM
uses a bounded roughly half-second queue, so the 263-second ending never becomes a whole-file allocation.
Video-only movies use the monotonic playback origin. Audio-bearing movies feed a per-instance
`AudioStreamGenerator`; the monotonic-clamped Godot sound-hardware position
(`playback + time-since-mix - cached output latency`) advances due video frames. Completion requires video
stop time, audio decode EOF, and PCM submission. Disposal interrupts waits, joins both workers, and stops the
generator without depending on native default-device playback.
`StopTimeMs` performs a bounded packet scan over the seekable in-memory payload and takes the longest usable
FFmpeg format duration, video-stream duration, timestamp span, or constant-frame-rate packet-count duration. This
@@ -99,12 +101,11 @@ safety rule still converts any backend initialization failure into an
explicitly completed zero-duration movie identity, and the presentation watchdog remains a last-resort guard for
an initialized backend that never reaches EOF.
The factory is injection for tests and future decoder replacement, not runtime codec roulette. FFmpeg is now the
selected Windows-x64 live backend; once its corpus and live gates pass, DirectShow is deleted rather than shipped
as a fallback. Modal
completion/cancel remains owned above the decoder by the existing `0x20f` host path. A future audio implementation
will consume timestamped PCM and may become the presentation clock; it must not change the VM-facing stop-time,
surface, or cancellation contracts.
The factory is injection for tests and future decoder replacement, not runtime codec roulette. FFmpeg is the
sole backend; DirectShow was deleted rather than shipped as a fallback after the live gate passed. Modal
completion/cancel remains owned above the decoder by the existing `0x20f` host path and tears down its generator
with the video instance. Native movie flag overrides select mute/music/SE/voice audio buses; the ordinary path
uses the movie bus.
This replacement is now also required for Windows gameplay parity. Archive-backed probes on 2026-07-21
show that the current DirectShow graph accepts tested MPEG widths divisible by 16 (208, 288, 304, 400, and
@@ -120,6 +121,18 @@ but only decodes Layer II audio and explicitly ignores program-stream PTS in its
FFmpeg covers the complete installed codec set and leaves the mod/profile boundary open without selecting a
different decoder per effect.
ABI v2 and MPEG audio playback landed on 2026-07-25. The expanded installed-corpus gate passes all 213 assets:
184 remain strictly video-only and all 29 audio-bearing streams decode to finite 44.1 kHz stereo PCM with
monotonic timestamps and clean EOF/teardown. The gate decoded 17,537 audio blocks / 18,185,856 stereo PCM
frames in the same eight-second unpaced run. A headless natural boot completed audio-clocked `LOGO.AGF` at
7,288 ms, released it without generator errors, and opened `OP.AGF`. A first windowed run confirmed audible
LOGO/OP output but exposed crackle/warble. The decoded PCM was continuous; the sink had treated each whole-
millisecond PTS rounding error (up to 44 samples at 44.1 kHz) as a real boundary and inserted or removed samples
from every MPEG block. The sink now ignores established-timeline discrepancies within 2 ms while still honoring
the first timestamp and material later gaps/overlaps. A 4,093-block OP regression pins splice-free continuity.
The clean audible recheck passed for LOGO/OP and CHAPTER. DirectShow and the managed Windows platform annotations
were then deleted; missing, distorted, or unsynchronized audio on any of the 29 audio-bearing assets is now a bug.
Native deliverables are RID-specific and bundled with the Godot export; the runtime must not discover an
arbitrary system FFmpeg. The first implementation gate is Windows x64 because that is the current runnable
target, but the C ABI and loader paths must reserve Windows x64, Linux x64, macOS x64, and macOS arm64 from the
@@ -137,7 +150,8 @@ nondecreasing timestamps. Malformed input and repeated teardown are covered. Thi
packaging. `FfmpegMovieDecoder` now adds cancellable timestamp pacing and is the live selection. The Godot build
copies the shim, five required shared libraries, and FFmpeg license beside `Himegari.dll`; a natural SYSTEM4
smoke completed 7288 ms `LOGO.AGF`, opened 106919 ms `OP.AGF`, and published frames from both without loading
DirectShow; the user subsequently confirmed both opening movies work in normal windowed playback. Full
DirectShow; the user subsequently confirmed both opening movies work in normal windowed playback. DirectShow
was deleted after the later synchronized-audio gate. Full
export/source-offer packaging and non-Windows builds remain outstanding.
The Windows-x64 installed-corpus gate passed on 2026-07-22 with the pinned dependency: all 213 signature-selected
@@ -145,7 +159,8 @@ MPEG payloads opened and reached EOF under a 30-second per-item bound. It decode
display sizes (120x120 through 800x600), independently matched sequence-header dimensions, found positive
duration and frame-rate metadata, verified tightly packed RGBA size and nondecreasing timestamps, observed
changing imagery in every asset, and closed every session. The set includes all 184 video-only and 29 audio-bearing
streams; audio presence is detected but PCM remains intentionally undelivered. The 263-second `ED.AGF` was the
streams; at that video-only milestone audio presence was detected but PCM remained intentionally undelivered.
The later ABI-v2 gate above supersedes that limitation. The 263-second `ED.AGF` was the
slowest decode at 4.4 seconds in the deliberately unpaced gate, so its prior 30-second failure was test-harness
whole-frame hashing overhead rather than a decoder incompatibility. The disposable machine report is
`build/movie-corpus-ffmpeg.json`; invocation and report semantics are canonical in `docs/tools-reference.md`.