13 · Media & Data

Audio System

OpenAL sources over ALSA PCM, a 24-channel voice pool inherited from the PS2 era, table-driven engine sound, and the announcement choreography whose completion gates departure itself.

13.1The stack

game logic (SePlay / VoicePlay / MytrainPlay / BgmPlay / PcmPlay)
  → DALPlaySound (0x20ab60) — resolves .asn by name/pattern, computes volume+pan
    → 24-channel voice pool allocator (VagVoiceMan heritage)
      → OpenAL (ALSoundImpOAL): sources, buffers, streaming queues
        → ALSA PCM (snd_pcm_*) → hardware

The .asn container (ALSN, 32-byte header: channels, rate, loop points, PCM offset) usually wraps Ogg Vorbis payloads; libogg/libvorbis are compiled into the binary. Loop points make BGM and engine loops gapless.

13.2The 24-channel voice pool

A fixed pool of 24 channels with owner/flag/release semantics: channels are claimed per sound class (SE, voice, BGM, PCM), each tracks its source + buffer handles, and takeover rules (priority preamble in VoicePlay2 @ 0x201928) decide who preempts whom. The pool explains the game's mixing behavior: too many simultaneous sounds and the oldest low-priority channel is stolen.

13.3Volume model (fully resolved)

Five volume bytes in optiondata (+0x20 master BGM?/+0x24 SE/+0x26 voice/+0x28 engine/+0x2A announce/+0x2C pad; exact mapping in the appendix), applied as OpenAL gain = f(volume_byte) with pan computed from the sound's positional arguments in DALPlaySound.

13.4Announcements: the departure gate

The station announcement system is the one that gates gameplay:

FunctionAddressRole
StationAnnounce0x1e47e83-way record semantics: departure-side / arrival-side / door-close clips
PlayAnnounce0x1e46e4per-frame trigger: arms 出発 (departure) / 到着 (arrival) announcements by clock position
SetAnnounce0x1e4538resolves authored tables → arms syupatu/tochaku voice IDs
MakeAnnouceQuestion0x1e5b58conductor-mode quiz: splices station-name voice clips at runtime
PcmCheck0x1a8e14“is any required clip still playing?” — the gate CheckDoorClose blocks on

This is why audio completion is gameplay-critical (Chapter 6's deadlock): door close waits on PcmCheck(), the door melody waits on the clock-vs-voice window, and the departure interlocks assume clips finish. The 5-slot kanko (chime) queue (KankoMan rotation) sequences the platform melodies.

13.5Engine sound: table-driven synthesis

The traction-motor hum is not a recording: it's a looping buffer whose playback rate and volume track speed and notch via per-formation tables (engSeTbl, including a type-3 tagged union variant), plus the trainSeDat profile selected by the diagram's train record (+0x18 → startRunSe sets rail-tap volume scaling from CSWTCH_2215 @ 0x42a9c0). The 84-byte SeIntervalChg fade pool ramps volumes in/out over frames (e.g. tunnel entry, passing trains).

13.6Per-frame ambient dispatcher

controlGameSe (@ 0x1e0464) runs every active frame over a 15-slot control block (SeVoiceCtlBlock): flange squeal on curves, rail joints at distance intervals, wind, tunnel boom, coupling proximity alerts (0x8b/0x8e), and the only two positional-audio sites (passing-train whoosh via the Mytrain family). Rain sound has full plumbing but, like weather physics, no live trigger exists in this build (the documented negative result).

Reimplementation note Stock OpenAL-soft + ALSA satisfies the whole contract, provided source-state transitions complete: the game polls AL_STOPPED-class states rather than callbacks. A backend that never advances source state (a lazy stub) will deadlock departure: the exact failure this report reproduced live.
← 12 · Rendering Pipeline