Audio System
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:
| Function | Address | Role |
|---|---|---|
| StationAnnounce | 0x1e47e8 | 3-way record semantics: departure-side / arrival-side / door-close clips |
| PlayAnnounce | 0x1e46e4 | per-frame trigger: arms 出発 (departure) / 到着 (arrival) announcements by clock position |
| SetAnnounce | 0x1e4538 | resolves authored tables → arms syupatu/tochaku voice IDs |
| MakeAnnouceQuestion | 0x1e5b58 | conductor-mode quiz: splices station-name voice clips at runtime |
| PcmCheck | 0x1a8e14 | “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).
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.