Sound Architecture
See also: Warcraft III — Unit Sound System.
Based on Quake 2's sound system. Sound is a client-side subsystem with server-mediated triggering via configstrings and entity state events.
Entity Sound Events (One-Shot)
Unit sounds (attack, death, movement) are delivered through entityState_t.event and entityState_t.sound — the same delta-compressed entity snapshot used for position and model. This avoids a separate svc_sound packet channel.
Protocol
- Server game code registers a WAV file path via
gi.SoundIndex(path)→ index stored inedict->sound_attack/sound_deathat spawn. - On the trigger frame, game sets
ent->s.event = EV_ATTACK(orEV_DEATH) andent->s.sound = sound_attack. Both fields are zeroed at the top ofG_RunEntitiesso they are non-zero for exactly one snapshot. - Client (
cl_parse.c) callsCL_EntityEvent(ent)after applying each entity delta ifent->event != 0. CL_EntityEvent(cl_fx.c) resolvescl.configstrings[CS_SOUNDS + ent->sound]to a file path and callsS_PlaySoundAt(path, &ent->origin2)for positional playback.S_PlaySoundFileloads the WAV from the MPQ (with LRU cache) and mixes it into an SDL audio channel.
Event Types (entity_event_t in common/shared.h)
| Value | Name | Trigger |
|---|---|---|
| 0 | EV_NONE |
No event |
| 1 | EV_ATTACK |
Attack swing began |
| 2 | EV_DEATH |
Unit died |
| 3 | EV_MOVE |
Footstep / movement sound |
| 4 | EV_ACK |
WC3 local selection/order acknowledgement |
| 5 | EV_OWNER_SOUND |
WC3 positional one-shot filtered to the entity owner |
WC3 Sound Registration
At map load, G_RegisterUnitSounds reads the unit's usnd label from unitUI.slk and registers authored What, Yes, Ready, YesAttack, and death assets. WC3 also loads UnitCombatSounds.slk and UISounds.slk; construction-complete and command-error sounds resolve through the local player's war3skins.txt fields into UISounds.slk. See docs/games/warcraft-3/sounds.md for the full lookup chains and current gaps.
EV_ACK and EV_OWNER_SOUND are server-authored like the other entity events, but WC3's G_CustomizeEntity filters their snapshots per client: acknowledgements require that client to have selected the unit, while owner sounds require ent->s.player == client player. World events such as death and tree impacts are not filtered this way.
Key Files
| File | Role |
|---|---|
games/warcraft-3/game/g_monster.c |
G_RegisterUnitSounds — sound index registration at spawn |
games/warcraft-3/game/g_sound.c |
WC3 UISounds.slk, owner-only sounds, and command-error sound dispatch |
games/warcraft-3/game/g_events.c |
G_RunEntities — clears s.event/s.sound each frame |
client/cl_fx.c |
CL_EntityEvent — fires sounds on event |
sound/s_sound.c |
S_PlaySoundFile — raw MPQ path playback |
Assets
Standard mono WAV files stored under sound/ relative to the game search path. Names are relative paths (e.g. "infantry/infpain1.wav" resolves to sound/infantry/infpain1.wav).
Sexed sounds use a * prefix (e.g. "*pain25_1.wav") and are resolved per player model at play time to #players/<model>/<base>, falling back to player/male/<base>.
Registration (two-tier)
Server side
SV_SoundIndex(name) (server/sv_init.c) assigns a 1-based index and stores the name in sv.configstrings[CS_SOUNDS + i]. The game DLL calls this through gi.soundindex(name).
Client side
CL_RegisterSounds() (client/cl_parse.c) iterates all CS_SOUNDS configstrings and calls S_RegisterSound() for each. This creates an sfx_t record but does not load WAV data until the sound is actually played (lazy loading).
Playing a sound
Game code calls:
Pipeline
| Step | Function | File | Description |
|---|---|---|---|
| 1 | gi.sound() |
game DLL | Game initiates the sound |
| 2 | PF_StartSound() |
server/sv_game.c |
Wraps entity, calls SV_StartSound |
| 3 | SV_StartSound() |
server/sv_send.c |
Encodes svc_sound, multicasts via PHS |
| 4 | CL_ParseStartSoundPacket() |
client/cl_parse.c |
Client reads the network message |
| 5 | S_StartSound() |
client/snd_dma.c |
Validates, loads WAV if needed, creates playsound_t, inserts into sorted pending queue |
| 6 | S_PaintChannels() |
client/snd_mix.c |
Mixer picks up playsounds when their time arrives |
| 7 | S_IssuePlaysound() |
client/snd_dma.c |
Picks channel, spatializes, assigns to channel_t |
| 8 | S_PaintChannelFrom8/16() |
client/snd_mix.c |
Mixes PCM samples into paintbuffer with volume scaling |
| 9 | S_TransferPaintBuffer() |
client/snd_mix.c |
Writes mixed samples into DMA output buffer |
| 10 | SNDDMA_Submit() |
win32/snd_win.c / linux/snd_linux.c |
Submits DMA buffer to audio driver |
Pain / Hurt sounds
Player pain
Triggered in P_DamageFeedback() (game/p_view.c:132), not in player_pain() (which is empty). Called at end of each frame:
r = 1 + (rand()&1);
player->pain_debounce_time = level.time + 0.7; // 0.7 sec throttle
if (player->health < 25) l = 25;
else if (player->health < 50) l = 50;
else if (player->health < 75) l = 75;
else l = 100;
gi.sound(player, CHAN_VOICE,
gi.soundindex(va("*pain%i_%i.wav", l, r)), // e.g. "*pain50_2.wav"
1, ATTN_NORM, 0);
Pre-registered at map load in game/g_spawn.c:
gi.soundindex("*pain25_1.wav"); gi.soundindex("*pain25_2.wav");
gi.soundindex("*pain50_1.wav"); gi.soundindex("*pain50_2.wav");
gi.soundindex("*pain75_1.wav"); gi.soundindex("*pain75_2.wav");
gi.soundindex("*pain100_1.wav"); gi.soundindex("*pain100_2.wav");
Monster pain
Each game/m_*.c file caches pain sound indices at spawn and plays them from AI pain frames:
// Registration (spawn)
sound_pain1 = gi.soundindex("infantry/infpain1.wav");
sound_pain2 = gi.soundindex("infantry/infpain2.wav");
// Trigger (pain AI frame)
gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0);
Key structs
| Struct | File | Purpose |
|---|---|---|
sfx_t |
client/snd_loc.h |
Sound asset record: name, registration sequence, cache pointer |
sfxcache_t |
client/snd_loc.h |
Loaded/resampled PCM data: length, loopstart, speed, width, trailing sample data |
playsound_t |
client/snd_loc.h |
Pending sound in the queue: sfx, volume, attenuation, entity, origin, begin time |
channel_t |
client/snd_loc.h |
Active mixing channel: sfx, left/right volume, end time, entity info |
wavinfo_t |
client/snd_mem.c |
Parsed WAV header: rate, width, stereo, loopstart, numframes |
Design principles
- Lazy loading: WAV data is not loaded until the sound is first played or during
S_EndRegistrationcleanup. - Playsound queue:
S_StartSounddoes not play immediately. It creates aplaysound_tsorted by time intos_pendingplays. The mixer picks them up when their time arrives. - 32 mixing channels (
MAX_CHANNELS).S_PickChanneluses priority: same entity+channel always overrides; monster sounds never override player sounds; otherwise the channel with least time remaining is evicted. - Spatialization: Every frame,
S_Updatere-spatializes all active channels based on listener position. Sounds from the player entity always play at full volume. - Configstring indexing: Game code uses integer indices; server stores names in configstrings; client resolves indices to
sfx_tpointers viacl.sound_precache[].
Key files
| File | Role |
|---|---|
client/sound.h |
Public API: S_Init, S_StartSound, S_RegisterSound, etc. |
client/snd_loc.h |
Private structs: sfx_t, sfxcache_t, playsound_t, channel_t |
client/snd_dma.c |
Core manager: registration, S_StartSound, channel picking, spatialization |
client/snd_mem.c |
WAV loading and caching: S_LoadSound, GetWavinfo, ResampleSfx |
client/snd_mix.c |
PCM mixing: S_PaintChannels, S_PaintChannelFrom8/16 |
client/cl_parse.c |
Client network: CL_ParseStartSoundPacket, CL_RegisterSounds |
server/sv_send.c |
Server: SV_StartSound — encodes and multicasts sound events |
server/sv_init.c |
SV_SoundIndex — configstring index assignment |
server/sv_game.c |
PF_StartSound + import table wiring (gi.sound, gi.soundindex) |
game/game.h |
game_import_t: gi.sound, gi.soundindex, gi.positioned_sound |
game/p_view.c |
Player pain sound trigger: P_DamageFeedback() |
game/m_*.c |
Monster pain sounds (each monster file) |