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 engine branch. Outside  Portal 2, they are supported in
 Portal 2, they are supported in  Counter-Strike: Global Offensive and
 Counter-Strike: Global Offensive and  Dota 2 (which now uses
 Dota 2 (which now uses  Source 2; the operator stacks have been greatly expanded upon and many of their names have been changed),
 Source 2; the operator stacks have been greatly expanded upon and many of their names have been changed),  Day of Infamy, and
 Day of Infamy, and  Insurgency.
 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.
 Note:We may refer to "this sound entry" in the context of a sound operator; It means the sound entry inside which the operator is used.
Note:We may refer to "this sound entry" in the context of a sound operator; It means the sound entry inside which the operator is used."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...
		}
	}
}
 Note:Soundscripts and thus operator stacks are written in the KeyValues format where quotation marks
Note:Soundscripts and thus operator stacks are written in the KeyValues format where quotation marks "..." 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_stackstart_stack | Executed once when the sound entry is started, in this order. For ambient_generic, this is when it receives the PlaySoundinput. | 
| update_stack | Executed regularly during playback.  Note:Adding an empty update_stackplays no sound at all. That's normal. You likely want to start everyupdate_stackby importing a stack that targets speakers, such asimport_stackwith a value ofp2_update_defaultorupdate_music_stereo. Look throughscripts\sound_operator_stacks.txtto 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 StopSoundinput. Note that this does not necessarily stop the sound immediately if the operatorsys_outputwithoutput stop_holdis 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.
 Note:We use the term "operator type" to avoid confusion. It may be tempting to also call it the "operator name", but otherwise we have two different things with the same term.
Note:We use the term "operator type" to avoid confusion. It may be tempting to also call it the "operator name", but otherwise we have two different things with the same term.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.  Note:This does not spatialize sounds. Apparently you need an ambient_genericto test that. Useent_fire <ambient_generic_name> playsoundinstead. | 
| stopsound | Stops all current sounds. This seems to be the only command to stop looping sounds that were started from the console. | 
| ent_fire ambient_generic stopsound | Fires the StopSoundinput to allambient_generics in the map. This is required for loopingambient_generics to play again if their sound has been stopped withstopsoundbecause that leaves the entity in a wrong internal state: It thinks that it is still playing, so it does not react toPlaySound. | 
| playsoundscape Nothing | Plays the soundscape "Nothing", which should exist in any game and contain nothing. Note that stopsoundstops all sounds that are currently playing, but the current soundscape may containplayrandoms which are designed to start new sounds from time to time, which can be disturbing for testing. Moving around the map may cause the current soundscape to change again. | 
| 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
 | 
| clear | Clears the console. Useful to find the start and end of debug text that may come from playing a sound entry. | 
 Tip:When modifying and testing a sound entry, it may be helpful to chain commands on one line to reduce the chance of getting lost when repeating commands using the up and down arrow keys in the console. Examples:
Tip:When modifying and testing a sound entry, it may be helpful to chain commands on one line to reduce the chance of getting lost when repeating commands using the up and down arrow keys in the console. Examples:
- stopsound; clear; sv_soundemitter_flush; playgamesound "Sound.Entry"
- ent_fire <ambient_generic> stopsound; clear; sv_soundemitter_flush; ent_fire <ambient_generic> playsound
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.
 Note:The stack
Note:The 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 wave path (*music/file.wav), and to include one of the predefined stacks
  - update_music_stereo(  - CS_update_music_stereo) for music that "plays everywhere" or
  - update_music_spatialfor music coming from a location in the world, typically an ambient_generic.
These stacks are very powerful and useful. For example, they account for sound occlusion and volumes of the music soundmixer and the snd_musicvolume convar. This is why the drymix sound character #, which is typically used for music, is generally not required if one of these stacks is present. They also contain useful operators that one can override, which solve fade in, fade out, LFO, DSP and spatializing:
update_stack
{
	import_stack "update_music_stereo" // or "update_music_spatial"
	volume_fade_in      // Operator name
	{
		input_max 3.0   // Attribute, your value
	}
}
The following tables list the most useful attributes to be overridden like in the above example. Below are more complex examples.
| 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 tmakes the volume oscillate with a cycle of|t| * 2 * PIseconds. To get a specific cycle in seconds, use that number times 6.2831853. (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 between1.0and1.0-xtimes 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 | 
|---|---|---|---|
| position_array | input_position_0[0]input_position_0[1]input_position_0[2] | @source_info.output_position | The x, y and z coordinate of the location that the music is emitted from. By default, this is simply the location of the sound emitting entity, such as an ambient_generic. Overriding these attributes allows moving the music source without recompiling the map.  Note:Sound entries spatialized with this cannot be tested with playgamesoundbecause it doesn't play them spatialized. The sound must come from anambient_genericthat may need to be stopped and started for each test, see above. | 
| input_position_1input_position_2input_position_3input_position_4input_position_5input_position_6input_position_7 | (0,0,0) each | Up to 7 other positions to emit the same sound from. To use these, it is also required to override input_entry_countto the number of used locations. To set the positions from x, y and z coordinates, access each position's coordinates withinput_position_N[0],input_position_N[1]andinput_position_N[2]. | |
| input_entry_count | 1 | The number of sound sources to use from position_array. If set toN, then the positionsinput_position_0throughinput_position_(N-1)will be used. | |
| dsp_output | input_float | 0.0 | Sets the DSP amount (0-1) via sys_output. | 
| speakers_spatialize | input_radius | 300 | Sets the distance to the music source at which the sound is entirely directional, see calc_spatialize_speakers. | 
// Any music should import an update_music stack.
"Music.Track_01"
{
	channel		CHAN_STATIC
	soundlevel	SNDLVL_NONE // means 'play everywhere', otherwise use "SNDLVL_70DB" etc.
	volume		1.0
	wave		"*music/file.wav" // note the * prefix
	soundentry_version 2
	operator_stacks
	{
		update_stack
		{
			import_stack "update_music_stereo" // used for music that 'plays everywhere', otherwise use "update_music_spatial"
			// optional: use the above table to look up operators that we may want to override
			volume_fade_in
			{
				input_max 3.0
			}
		}
	}
}
// Spatialized music. Remember to set 'soundlevel', too.
update_stack
{
	import_stack "update_music_spatial"
	
	position_array
	{
		input_entry_count 2	// if set to N, the game plays the sound at the positions 0 through N-1
							// position_0 defaults to the sound source, the others to (0,0,0)
		// input_position_0 overrides the actual ambient_generic's position
		input_position_0[0] -700.0 // x0
		input_position_0[1] -700.0 // y0
		input_position_0[2]  100.0 // z0
		// additional position
		input_position_1[0]  600.0 // x1
		input_position_1[1]  600.0 // y1
		input_position_1[2]  200.0 // z1
	}
	speakers_spatialize
	{
		input_radius 300.0 // distance from sound source at which the sound is fully directional
	}
	dsp_output
	{
		input_float 0.00 // how much DSP, from 0.0 to 1.0
	}
}
Synchronization
Suppose music.b0 is already looping and we want to start music.b1 in sync (i.e. at the same time position in its audio), then it needs the following start_stack. This is used especially for laser music in Portal 2 or for a piece of music split into phases that are played one after the other with no stops or jumps in the music.

 
 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
	}
}
 Bug:If the second music (supposed to start synchronized) overrides volume_fade_in, it won't synchronize properly, instead being late by some audible milliseconds, even though the start_stack calculates the correct offset. The longer the fade in time, the worse the effect; A fade time of 1.0 may still be acceptable. If applicable, instead of having multiple synchronized tracks playing at the same time, it may be preferable to create a mixed version and fade to that.
Bug:If the second music (supposed to start synchronized) overrides volume_fade_in, it won't synchronize properly, instead being late by some audible milliseconds, even though the start_stack calculates the correct offset. The longer the fade in time, the worse the effect; A fade time of 1.0 may still be acceptable. If applicable, instead of having multiple synchronized tracks playing at the same time, it may be preferable to create a mixed version and fade to that.  )
)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.
 Tip:The tool mentioned above may be in the files of
Tip:The tool mentioned above may be in the files of  Dino D-Day.
 Dino D-Day.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
}