Soundscripts/Operator Stacks
Operator stacks are a system for soundscripts allowing to add complex behaviour to sounds.
They are unrelated to Soundscapes.
Operator stacks were introduced in the Portal 2 engine branch. Outside Portal 2, they are supported in Counter-Strike: Global Offensive and Dota 2 (which now uses Source 2; the operator stacks have been greatly expanded upon and many of their names have been changed), Day of Infamy, and Insurgency.
Syntax
Any soundscript can use operator stacks by specifying the key-value pair soundentry_version 2
as well as a keyvalue named operator_stacks
.
"Entry.Name"
{
channel CHAN_STATIC
soundlevel SNDLVL_NONE
volume 1.0
wave "common/null.wav"
soundentry_version 2
operator_stacks
{
prestart_stack // optional
{
// sound operators...
}
start_stack // optional
{
// sound operators...
}
update_stack // optional
{
// sound operators...
}
stop_stack // optional
{
// sound operators...
}
}
}
"..."
around tokens are always optional if the token does not contain any whitespace. In your code, you are free to use or omit quotes in any way you want. Thanks to syntaxhighlighting, we can use quoted strings on this page in a sparing but meaningful way to emphasize:
- sound entry names (
"Sound.Entry"
), - file paths (
"sound/file.wav"
), - operator types (
"sys_stop_entries"
) and - operator stack names when using
import_stack
("p2_update_default"
).
Stack types
There are four types of stacks, each triggered at different times. Inside these stacks, one can put one or multiple sound operators.
prestart_stack start_stack
|
Executed once when the sound entry is started, in this order. For ambient_generic, this is when it receives the PlaySound input.
|
update_stack
|
Executed regularly during playback.
Note:Adding an empty
update_stack plays no sound at all. That's normal. You likely want to start every update_stack by importing a stack that targets speakers, such as import_stack with a value of p2_update_default or update_music_stereo . Look through scripts\sound_operator_stacks.txt to find a fitting base for your purpose. |
stop_stack
|
Executed once when the sound entry ends or is stopped. For ambient_generic, this is when it receives the StopSound input. Note that this does not necessarily stop the sound immediately if the operator sys_output with output stop_hold is used, allowing for concepts like fadeout.
|
Sound operators
Sound operators (or just operators) are keyvalues inside any of the stack types and consist of the following.
// Pseudo code ("<...>" represents a string):
<operator_name>
{
operator "<operator_type>"
<attribute1> <value1>
<attribute2> <value2>
// ...
}
// may define an output value: @<operator_name>.<output1>
|
// Concrete example:
add_floats
{
operator "math_float"
apply add
input1 3.0
input2 5.0
}
// defines the output value: @add_floats.output (hopefully 8.0)
|
This entire structure is what we call a "sound operator" (or just "operator" for short), consisting of an "operator name", an "operator type" and its "operator attributes". A sequence of operators form an "operator stack". The order of operators matters.
Similar with the term "operator" which we use to describe the entire sound operator keyvalues (that one can refer to by its operator name). By "operator" one might also mean an operator type.
Operator name |
The name of an operator can be an arbitrarily chosen string. This name is needed to reference an operator's output ("which operator?") and for debug printing purposes. There is special behavior going on if there are two operators with an equal name: The latter will override any attributes of the former (by merging the latter keyvalues into the former). When #Importing stacks, overriding operators by choosing an equal operator name is intentional. Otherwise, equal operator names are most likely unnecessary or – if overlooked – cause problems and should thus be avoided. | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
Operator type |
The operator type determines what the operator does and which attributes and outputs it has. A list of available operators can be obtained with the console command Main article: Sound operators
| |||||||||||
Operator attributes |
Depending on the operator type, the operator has some set of internal attributes with some value (floats, strings, ...) which can be set to either a constant expression such as
| |||||||||||
Operator outputs |
Furthermore, an operator may have outputs depending on the operator type. As opposed to attributes, outputs can be read and not set. Outputs receive their values after an operator has finished execution (i. e. their value is undefined in the text editor). |
Example for a start_stack
referencing the operator types math_random
and sys_output
:
"Entry.Name"
{
// ...
operator_stacks
{
start_stack
{
random_number // operator name (arbitrary)
{
operator "math_random" // operator type. "math_random" stores a random number (-3 <= x <= 3) inside '@random_number.output'
input_min -3.0 // lower boundary for the random value
input_max 3.0 // upper boundary for the random value
}
apply_delay // operator name (arbitrary)
{
operator "sys_output" // operator type. The effect of "sys_output" depends on its attribute named 'output'
// (which is not an operator output in this case)
output delay // applies a delay to the sound (negative delay = skip into the sound)
input_float @random_number.output // accesses the output of the previous operator 'random_number' which (also) has the name 'output'
}
}
}
}
Importing stacks
Instead of always starting from scratch, one can use the import_stack
command to extend a predefined template.
There are 28 operators used in Portal 2, but they are re-configured and combined in hundreds of different ways. Most of the resulting "stacks" are very specific, and this page will only deal with the more general ones.
Inside any operator stack, one can add the statement import_stack <stack_name>
where <stack_name>
is one from sound_operator_stacks.txt
. One can imagine that its effect is that the text is copied over. The clue is that any operator (including imported ones of course) can be overridden in their values (see the examples below). That's why predefined operator stacks typically define operators with constant changes, null sound entry names or things like a fade time of 0 seconds: After importing, one only needs to override the right value to get a fade time of 3 seconds, because the "hard work" of performing a volume fade is already solved by the imported stack!
Inside "stop_and_play"
{
play_entry
{
operator "sys_start_entry"
execute_once true
input_execute 1
input_start 1
entry_name "Default.Null" //Replace with the sound you want to play.
}
}
| ||||
// What you type:
import_stack "stop_and_play"
play_entry
{
entry_name "Player.PickupWeapon"
}
|
→ |
// What it becomes after copying over:
play_entry
{
operator "sys_start_entry"
execute_once true
input_execute 1
input_start 1
entry_name "Default.Null"
}
play_entry
{
entry_name "Player.PickupWeapon"
}
|
→ |
// What it becomes after merging:
play_entry
{
operator "sys_start_entry"
execute_once true
input_execute 1
input_start 1
entry_name "Player.PickupWeapon" // <-- overridden
}
|
Full sound entry example using import_stack
on P2_exclusion_time_blocker_start
:
VFX.LightFlickerEnd
{
channel CHAN_AUTO
soundlevel SNDLVL_105db
volume 1.0
rndwave
{
wave "vfx/light_flicker/light_flicker_end_01.wav"
wave "vfx/light_flicker/light_flicker_end_02.wav"
wave "vfx/light_flicker/light_flicker_end_03.wav"
wave "vfx/light_flicker/light_flicker_end_04.wav"
}
soundentry_version 2
operator_stacks
{
start_stack // applied when the sound begins
{
import_stack "P2_exclusion_time_blocker_start" // defined in scripts/sound_operator_stacks.txt
// We are now extending/configuring "P2_exclusion_time_blocker_start"
// which contains an operator with the exact name "block_entries" whose values we will now override:
block_entries // prevents another sound from playing
{
input_duration 0.25 // seconds to block for
match_entry "World.LightFlickerEnd" // the sound entry to block
match_entity false // only on the same entity that this sound is playing from?
}
}
}
}
Debugging tools
util_print_float
There is the operator type util_print_float
to write a float value to the console along with its operator name.
If it is no longer needed, this operator can safely be commented out with no other impact.
print_operator
{
operator "util_print_float"
input 123 // more useful: "@<other_operator>.<output>"
}
// will print to the console:
// "SOS PRINT FLOAT: print_operator: 123.000000"
Useful console commands
In-game, there are various useful console commands or convars to show debug information about sounds or sound operators being executed. Note the abbreviation sos
, presumably for Sound Operator System. There are more commands starting with snd_sos_*
.
playgamesound <sound_entry> |
Plays a sound entry. |
stopsound |
Stops all current sounds. |
snd_show 1 |
Shows all current sound channels along with file path, sound entry, duration and volumes. |
sv_soundemitter_flush |
Reloads all soundscripts. Useful to apply changes to a soundscript without having to restart the game. |
|
Shows the state of all operators for the different stack types, including existing properties and their current values. For example, when the above operator is executed in a start_stack , the following output will be written to the console:
Name: print_operator Execute Once: false input_execute: 1.000000 input: 123.000000 |
Examples
Start another sound
Plays another sound entry. The following two examples are equivalent.
sys_start_entry
, stop_and_play
play_other_entry // arbitrary name
{
operator "sys_start_entry"
entry_name "VFX.FizzlerDestroy" // sound entry to start
input_start 1
}
import_stack "stop_and_play" // this is defined under the stop_stacks in Portal 2, hence
// the misleading name. importing this does not stop anything.
play_entry // overriding, not arbitrary!
{
entry_name "VFX.FizzlerDestroy" // sound entry to start
}
Limit the number of sounds
Limits the maximum number of sounds that can be played at once, either by sound name or entity. A sound will not stop itself from playing.
P2_poly_limiting_start
start_stack
{
import_stack "P2_poly_limiting_start"
limit_sound
{
match_entry "VFX.OGSignFlicker"
input_max_entries 3.000000
}
}
Crop a sound
The following skips the first 7.10
seconds of the sound file and plays only for 0.50
seconds. An alternative solution takes the time stamp, not the duration. If only one end of the sound should be cropped, you can remove the appropriate stack.
update_stop_at_time
takes a sound duration to define the stop time, p2_update_stop_at_elapsed
takes a time stamp. The difference comes from the former using get_entry_time
's output output_entry_elapsed
and the latter using output_sound_elapsed
instead.sys_output
, p2_update_default
, update_stop_at_time
start_stack
{
// skips the first 7.10 seconds of the sound file
crop_start
{
operator "sys_output"
input_float -7.10 // note the negative delay
output delay
}
}
update_stack
{
// plays only 0.50 seconds of the entry
import_stack "p2_update_default"
import_stack "update_stop_at_time" // alternative: "p2_update_stop_at_elapsed"
usat_stop_time // alternative: time_elapsed_trigger
{
input2 0.50 // alternative: 7.60
}
}
Music
Observing the Portal 2 music soundscripts, it is generally recommended for music to use the stream sound character before its path (*music/file.wav
) and to include one of the predefined stacks update_music_stereo
or update_music_spatial
. These two already contain useful operators that one can override, which solve fade in, fade out, LFO, DSP and spatializing:
Operator name | Attribute | Default | Effect |
---|---|---|---|
volume_fade_in
|
input_max |
0.0 | Fade in time in seconds when the sound starts |
volume_fade_out
|
input_max |
0.0 | Fade out time in seconds after the entry is stopped |
volume_apply_adjust
|
input1 |
1.0 | Volume scale factor (will be clamped to sound system's maximum volume) |
volume_lfo_time_scale
|
input2 |
0.0 | LFO time scale factor. Setting it to t makes the volume oscillate with a cycle of |t| * 2 * PI seconds. To get a specific cycle in seconds, use that number times 6.283. (The underlying cos function accepts radians and receives seconds.)
|
volume_lfo_scale
|
input2 |
0.0 | LFO volume scale factor. Setting it to x (between 0.0 and 1.0) will make the volume oscillate between 1.0 and 1.0-x times the normal volume. If set to a value too close to 1.0, the sound may stop; use a lower value such as 0.98.
|
The following apply only to update_music_spatial
:
Operator name | Attribute | Default | Effect |
---|---|---|---|
dsp_output
|
input_float
|
0.0 | Sets the DSP value via sys_output .
|
speakers_spatialize
|
input_radius
|
300 | Sets the radius of the "sphere" around the music source inside which the sound becomes no longer directional (L/R volume levels equalize), see calc_spatialize_speakers .
|
// Any music should import an update_music stack
update_stack
{
import_stack "update_music_stereo" // or "update_music_spatial"
// loop up operators that we want to override in the above table
volume_fade_in
{
input_max 3.0
}
}
Synchronization
Suppose music.b0
is already looping and we want to start music.b1
(which has the same length) such that it plays in sync.
start_sync_to_entry
// We're inside the entry "music.b1"
start_stack
{
import_stack "start_sync_to_entry" // or "start_delay_sync_to_entry" to instead make "music.b1" wait until it can start in sync, without skipping into the music
elapsed_time
{
entry "music.b0" // find the music that is already playing
}
duration_div
{
input2 1 // default is 4; if "music.b1" is 1/x the length of music.b0, then use x
}
}
Moving sound
Smoothly transitions a sound from one location to up to eight others as the player moves through a map. An unreleased tool can auto-generate these. Position 1 is where the sound was emitted from.
p2_update_dialog_spatial_cave
import_stack "p2_update_dialog_spatial_cave"
position_array
{
input_entry_count 3
// position 2
input_position_1[0] 2129
input_position_1[1] -850
input_position_1[2] -1267
// position 3
input_position_2[0] 1473
input_position_2[1] -1200
input_position_2[2] -1343
}