Sound
Overview
Chisel’s sound system wraps OpenAL through SoundDevice. It handles 3D spatial audio, volume categories, and buffer caching. All audio files must be in OGG Vorbis format. 3D sounds are forced to mono by the loader; stereo is only preserved when spatialization is disabled.
Sounds are defined in SoundScripts. SoundScripts are text files that describe a named sound event (its file or list of candidate files, volume, pitch, distance falloff, channel, etc.), and played by name through SoundScriptManager.
Playing a Sound
The main API is SoundScriptManager.PlaySound(), called with the name of a SoundScript entry:
SoundScriptManager.PlaySound(
"Weapon.SMG.FireShot",
position,
is3DOverride: Owner is not Player
);
The full signature:
name The SoundScript entry to play, e.g. "Weapon.SMG.FireShot" or "Physics.crate.Impact". See below for the file format.
position (Vector3.Zero) World position to play the sound from.
velocity (Vector3.Zero) Velocity of the sound source, stored on the returned SoundInstance for later use (e.g. Doppler-style updates as an object keeps moving).
is3DOverride Overrides the script’s is3D value for this call. Useful for playing the same script both spatially and flatly depending on who’s making the sound, like a weapon that should be spatialized when an NPC fires it but flat when the player does.
overrideVolume Overrides the script’s evaluated volume for this call. PhysicsSounds uses this to scale impact volume by collision speed.
overridePitch Overrides the script’s evaluated pitch for this call.
PlaySound returns a SoundInstance (or null if the entry doesn’t exist or fails to evaluate. Errors are logged to the console rather than thrown). Use it to update the source’s position, velocity, or gain after it starts playing:
var instance = SoundScriptManager.PlaySound("Vehicle.Engine.Loop", entity.Position);
// later, once per frame while the source is following something that moves:
instance.SourcePosition = entity.Position;
Writing a SoundScript
SoundScripts live as plain text files in Scripts/sound in your content root, and are loaded from every file in that directory on startup. The exact filename doesn’t matter, only the entry names inside. The format is Chisel’s generic script language.
Bare blocks Each named block is a definition. Weapon.SMG.FireShot { ... } defines an entry playable by that exact name.
= Assigns a property inside a block: channel = SFX, volume = 0.5.
: Inherits from another definition, pulling in all of its properties as defaults: Physics.oildrum.Impact : Physics { ... }.
$Name Declares a reusable constant, resolved by simple find-and-replace before parsing: $PhysicsPitch = random(0.85, 1.2), then referenced later as pitch = $PhysicsPitch.
#include "file" Splices another script file in at that point, relative to the including file’s directory. Included files are only processed once even if referenced from multiple places.
random(min, max) Function call. Evaluated fresh every time the entry is played — e.g. pitch = random(0.9, 1.1) gives each play a slightly different pitch.
select(random) { ... } / select(sequential) { ... } Function call with a trailing block of candidate values, most commonly used for sound to pick one of several files. random picks uniformly at random each play; sequential cycles through the list in order.
A full example, combining a shared base definition with inheritance and a randomized file pick:
$PhysicsPitch = random(0.85, 1.2)
Physics
{
channel = SFX
pitch = $PhysicsPitch
}
Physics.crate.Impact : Physics
{
sound = select(random)
{
"Audio/Physics/crate/impact_crate-01.ogg",
"Audio/Physics/crate/impact_crate-02.ogg",
"Audio/Physics/crate/impact_crate-03.ogg"
}
}
The recognized properties on any entry:
channel Which volume category the sound belongs to: SFX, Music, or anything else (treated as Master). Same categories as before, still mapped to the options menu’s volume sliders.
sound The file (or select(...) block of candidate files) to play, path relative to your content root.
volume (1.0) Volume multiplier, evaluated per play.
pitch (1.0) Playback speed multiplier, evaluated per play.
loop (false) Whether the sound loops.
is3D (true) Whether the sound is spatialized. Set to false for UI sounds and anything else that should play at a flat volume everywhere.
minDistance (1) Distance within which the sound plays at full gain.
maxDistance (64) Distance beyond which the sound is inaudible.
Weapon and one-off UI sounds are usually simple, flat definitions with no inheritance:
General.Accept
{
channel = SFX
volume = 0.5
is3D = false
sound = "Audio/accept.ogg"
}
To reuse another weapon’s sound as a stand-in while you’re stubbing something out, inherit from it and don’t override sound at all:
Weapon.Shotgun.FireShot : Weapon.SMG.FireShot
{
}
Checking Whether a Script Exists
Use SoundScriptManager.Exists(name) before playing something that’s only conditionally defined, rather than relying on the try/catch inside PlaySound. This is how the physics impact system decides whether a given material has an impact sound at all:
var scriptname = $"Physics.{physName}.Impact";
if (!SoundScriptManager.Exists(scriptname)) return;
SoundScriptManager.PlaySound(scriptname, position, velocity, overrideVolume: volume);
Prefetching Sounds
The first time a sound file is played, the engine reads and decodes the OGG from disk and caches the PCM data in a buffer. That initial decode can cause a small stutter on the first play. To avoid this, prefetch sounds you know you’ll need before they’re first played.
The right place to prefetch is your controller’s Prefetch() method, which is called once per class on map load before any instances exist:
public override void Prefetch()
{
SoundDevice.Device.PrefetchSound($"{GameEngine.Instance.Content.RootDirectory}/Audio/Weapons/smgfire.ogg", mono: true);
}
Prefetching loads and caches the buffer so the first actual play call hits the cache immediately instead of loading on the spot.
Listener Position
The sound system needs to know where the “ears” are so it can calculate 3D falloff and panning. The engine expects you to update the listener position each frame with the camera position and orientation. In the FPS template this is done by the player controller. If you’re using a custom camera, update it manually:
SoundDevice.Device.ListenerPosition = cameraPosition;
SoundDevice.Device.CameraForward = cameraForward;
SoundDevice.Device.CameraUp = cameraUp;
If you forget to update this, all 3D sounds will have their distance calculated from wherever the listener was last set, which is usually the world origin at startup.
Source Limits
The sound system has a pool of audio sources (default 256, changeable with snd_maxsources in the console). When the pool is full, new sounds steal the source that was least recently used. This means very loud maps can cause faint distant sounds to be dropped. Keep maxDistance values on your SoundScript entries reasonable so the system doesn’t waste sources on inaudible sounds.
A sound with a maxDistance smaller than the distance to the listener is skipped entirely without consuming a source at all.