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.
Note.pngNote:This does not spatialize sounds. Apparently you need an ambient_generic to test that.
stopsound Stops all current sounds.
Note.pngNote:This stops the sound channel of looping ambient_generics but does not update the state of that entity. They still need to be stopped with their StopSound input to play 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.

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.
Tip.pngTip: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.

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 wave path (*music/file.wav), and to include one of the predefined stacks

Among other things these stacks account for appropriate volumes (for example from the music soundmixer and the snd_musicvolume 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 ambient_generic. Overriding these attributes allows moving the music source without recompiling the map.
Note.pngNote:Sound entries spatialized with this cannot be tested with playgamesound because that doesn't play it spatialized. The sound must come from an ambient_generic that may need to be stopped and started for each test, see above.
position_array input_position_1
input_position_2
input_position_3
input_position_4
input_position_5
input_position_6
input_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 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 (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!

See also:  Portal 2Counter-Strike: Global Offensive 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