Soundscripts/Operator Stacks

From Valve Developer Community
Jump to navigation Jump to search

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 Portal 2 engine branch. Outside Portal 2 Portal 2, they are supported in Counter-Strike: Global Offensive Counter-Strike: Global Offensive and Dota 2 Dota 2 (which now uses Source 2 Source 2; the operator stacks have been greatly expanded upon and many of their names have been changed), Day of Infamy 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.pngNote: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.pngNote: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_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.pngNote: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.

Note.pngNote: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 snd_sos_print_operators. Examples are sys_output, math_float or get_entry_time.

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 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>

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
Attributes that all sound operators have in common
Attribute Default Description
input_execute 1.0 If set to a value ≤ 0.0, this operator is not executed, i. e. skipped.
execute_once false Whether the operator should run every time the stack is evaluated, or only the first time. Output values are presumably stored between executions in the latter case.
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.

Todo: Work out a sane way of documenting all this!

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:

"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.

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

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.

Note.pngNote: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.
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.

See also:  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.

Tip.pngTip:The tool mentioned above may be in the files of Dino D-Day Dino D-Day.
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