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 . Outside , they are supported in and (which now uses ; the operator stacks have been greatly expanded upon and many of their names have been changed), , and .
== 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...
		}
	}
}
=== 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 , this is when it receives the PlaySound input.
update_stack 	Executed regularly during playback.
stop_stack 	Executed once when the sound entry ends or is stopped. For , 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.
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 snd_sos_print_operators. Examples are sys_output, math_float or get_entry_time.		
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 1.0, "Default.Null", etc. or to the output of a previous operator using a preceding @ symbol, such as "@<operator_name>.<output>".
Every attribute has some default value, which is used whenever an attribute in an operator is omitted.
When trying to set an attribute that the operator doesn't know, the system writes a line to the console:
Error: Operator <operator_name>, unknown sound operator attribute <attribute>
 {| style="float:right; margin-left:1em; padding:0"	
rand_default
{
	operator		"math_random"
	execute_once	false
	input_execute	1
	input_min		0.0
	input_max		1.0
	input_seed		-1.0
}	
rand_default
{
	operator "math_random"
}
// equal to the left one
// due to default values
Example for a start_stack referencing the operator types and :
"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 , 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!
 What happens when importing a stack?
Inside portal2\scripts\sound_operator_stacks.txt, under stop_stacks, one can find :
"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.
	}	
}	
Inside portal2\scripts\sound_operator_stacks.txt, under stop_stacks, one can find :
"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.
	}	
}	
Inside portal2\scripts\sound_operator_stacks.txt, under stop_stacks, one can find :
"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.
	}	
}	
Inside portal2\scripts\sound_operator_stacks.txt, under stop_stacks, one can find :
"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.
	}	
}	
Inside portal2\scripts\sound_operator_stacks.txt, under stop_stacks, one can find :
"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 :
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 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.
snd_sos_show_operator_prestart <0/1>
snd_sos_show_operator_start <0/1>
snd_sos_show_operator_update <0/1>
snd_sos_show_operator_stop_entry <0/1>	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.
== Examples ==
=== Start another sound ===
Plays another sound entry. The following two examples are equivalent.
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.
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.
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
* ( ) for music that "plays everywhere" or
* for music coming from a location in the world, typically an .
Among other things these stacks account for appropriate volumes (for example from the music soundmixer and the convar; This is why the drymix sound character # is not required if one of these stacks is present), and 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.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 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
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 . Overriding these attributes allows moving the music source without recompiling the map.
position_array 	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_count to the number of used locations. To set the positions from x, y and z coordinates, access each position's coordinates with input_position_N[0], input_position_N[1] and input_position_N[2].
position_array 	input_entry_count 	1 	The number of sound sources to use from position_array. If set to N, then the positions input_position_0 through input_position_(N-1) will be used.
dsp_output 	input_float 	0.0 	Sets the DSP amount (0-1) via .
speakers_spatialize	input_radius 	300 	Sets the distance to the music source at which the sound is entirely directional, see .
// 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 (which has the same length) such that it plays in sync.
Remember using the stream sound character * for the sound file because otherwise its load time may defeat the purpose of the following!
// 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.
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
}
== See also ==
*Sound operators
*sound_operator_stacks.txt
*Soundscripts
*Sound and Music
Category:Sound System