Scripting with Variables
So we now know how to start and stop pieces of audio. For many scenarios, the next step is to be able to use application information to dictate what specific audio is triggered. This involves the use of variables, which are scripting objects that the programmer and the scripter "share" and can adjust or check. In the most basic examples, the programmer will be the only one adjusting a variable's value, and the script will read that value to decide what to do.
Let's return to the character introductions. What if we had 20 character names that our player could choose at the beginning of our application? We wouldn't want to write 20 routines that the programmer would have to choose between when it came time to introduce ourselves. We could instead use a variable — say, one called CharacterNumber (remember that variables, like other scripting objects, cannot contain spaces or start with a digit). The programmer sets this variable when we choose our name. Then whenever we introduce ourselves to another character, the programmer can call our single script routine, which checks the value and uses it to decide the appropriate wave to play.
Variable use consists of two parts. First, you must declare the variable so the script knows how to use it (similar to dragging a wave or Segment into one of the script's two folders). The format for this is simply:
Dim [name of variable]
One quick restriction relating to AudioVBScript is that you must declare any variables that you intend to use above any routines. So this would be illegal:
Sub EnterRoom RoomAmbience.Play (IsSecondary + AtMeasure) End Sub 'illegal - all "dim" s must come before all "sub" s dim CharacterNumber
Also make sure that you don't name a variable the same name as one of your other scripting objects (Segments, waves, and so on). Again, the Message Window will let you know if this problem occurs.
Once a variable has been declared, if you click away from the Sources tab, you'll see the variable appear in the lower-right frame of the Script Designer window, as in Figure 7-11.
Figure 7-11: Variables list in the lower-right frame of the Script Designer window.
You can now click on the value field to set the value, just as the programmer would do in the application. Clicking the Reinitialize button resets all values to Empty.
Now that we've created a variable, we use conditional statements to act based on their value. With conditional statements, the script performs some comparison on the variable to decide how to act (for instance, what piece of music to play and when to play it). The simplest conditional statements are if…then statements, where if a condition is true, you will perform one or more actions. The syntax for this is:
If (expression) then [Action1] [Action2] [...] End if
Incidentally, indenting the actions that occur within the If/End If statements are for readability only; there's no rule that these lines of code must be spaced a certain way (in fact, tabs and extra spaces are universally ignored in DirectX Audio Scripting).
As an example of a simple If/End If statement, we could have a script like this:
dim PlayerHealth Sub PlayDeathMusic if (PlayerHealth = 0) then DeathMusic.Play (AtImmediate) end if end sub
If the programmer tried to trigger the death music when the player still had health, nothing would happen; only if health was zero would we hear the Segment called DeathMusic play.
More complex statements will tend to use the if…then/elseif… then/else format: If a condition is true, do some action; otherwise, check to see if another condition is true (elseif) and perform their actions; otherwise, if all of these conditions are false (else), do some final action. For our character introduction example above, our code might look like this (let's simplify to three characters instead of 20):
dim CharacterNumber 'We have three possible options for our character '1=Bob '2=Joe '3=Bill Sub IntroduceCharacter 'We trigger this routine whenever our character meets 'other characters. Hello.play MyNameIs.play AtFinish if (CharacterNumber=1) then Bob.play (AtFinish) elseif (CharacterNumber=2) then Joe.play (AtFinish) elseif (CharacterNumber=3) then Bill.play (AtFinish) else Trace "Illegal character number." end if End Sub
We can test out the above routine by setting our variable in the lower-right frame of the Script Designer (just as the programmer would in application code) and then double-click a routine to trigger it, again, just as the programmer would.
Notice the final else statement in the above example. Using trace statements here often comes in handy for debugging; you are alerted that something caused your variable's value to go outside of the range that you expected it to fall into.
Any time a variable is declared with a dim statement, it is a global variable that the programmer can see and manipulate. There are often situations where global variables are not necessary. For instance, the scripter might do some basic math on several global variables to decide what piece of music to play. Rather than having to create another global variable for this value, scripters can use local variables. Local variables have two primary differences from global variables — they cannot be seen by the programmer, and they do not have to be declared. They are implicitly created when they are first used.
A good example of local variable use is adding randomization to scripted playback. AudioVBScript has a rand function that can be used to generate a random number from 1 to the value you specify. For instance, if we had three pieces of music that would be appropriate for the current scene, we could construct a script routine like the following:
Sub PlaySceneMusic x = rand(3) 'x is a local variable - we did not have a ' "dim x" statement. 'The programmer cannot see it, and its value is ' not stored after the routine finishes running. If (x=1) then LowLevelMusicThematic.Play ElseIf (x=2) then LowLevelMusicRhythmic.Play ElseIf (x=3) then LowLevelMusic.Play End if End Sub
Again, see the DirectMusic Producer documentation page (for Performance.Rand) for more details on this function. Of course, this provides a small subset of the functionality that you could create within a single Segment with variations, but it does allow for your script to randomly choose between several fully composed Segments without needing to merge them into a giant Segment or needing for the programmer to get involved.
A Two-Way Street: Variables Set by the Scripter
Variables can also be set by the scripter and used by the programmer. For instance, a sound designer could track how many times a routine has been called or how many instances of a sound have been started. Note that tracking when they've stopped is often more easily handled by the programmer.
dim NumberOfTorchesPlayed Sub StartTorch Torch.Play (IsSecondary) NumberOfTorchesPlayed = NumberOfTorchesPlayed + 1 end sub
Tracking Individual Instances of a Playing Segment
Now that we have a basic understanding of variables, let's return to the stop function on Segments. One thing to note with the Segment.Stop call is that we're stopping the Segment rather than a specific playing instance of that Segment. For ambience and music, where there is generally only one instance of any particular piece of music playing at a time, this isn't an issue. But let's say for example that we have a Segment that plays a torch sound, and the character in our application is walking through an environment with 50 torches that we had started playing. Calling Torch.stop would stop all 50 of them. If we have just extinguished one, we want the ability to stop just that single torch.
To do this, we can keep track of PlayingSegment objects. Whenever a Segment is played, we can choose to store this particular playing instance in a variable. We can later stop this particular PlayingSegment without stopping every other instance of the original Segment.
dim MyPlayingTorch Sub StartMyTorch 'We only carry one torch, so there's only one 'PlayingSegment we need to keep track of Set MyPlayingTorch = Torch.Play (IsSecondary) end sub Sub StartRoomTorch Torch.Play IsSecondary End sub Sub StopMyTorch 'Will only stop our own torch; room torches will 'continue to play. MyPlayingTorch.Stop End Sub Sub StopAllTorches Torch.Stop End Sub
Note that the syntax for using objects in variables is slightly modified from numeric variables — a Set statement is used when you are assigning something to an object, rather than a numeric variable. In this example, we now have a personal torch that we track the instance of, and if our character extinguishes it, that single instance can be stopped without affecting other torches in the room. Meanwhile, the room might be filled with other torches that we don't need to be able to stop individually, so the script doesn't keep track of their individual PlayingSegment objects. The StopAllTorches method would stop every torch, including our own.
Triggering Segment Playback Relative to Playing Segments
Now that we can use PlayingSegment objects, we can return to our original Character Introduction script routine (stitching together "Hello my name is Bob") and make it use secondary Segments. By default, when you play a Segment, it uses the primary Segment as the reference point to know when to start playing. But we don't want that here, or the "my name is" and "Bob" waves aren't going to play until the primary Segment finishes (and at that point, they'll both play at the same time!). We instead want "my name is" to queue up relative to the "Hello" Segment (which is going to play as a secondary Segment) and then for "Bob" to queue up relative to "my name is."
To get the proper stitching behavior, we are going to use the fourth parameter on the Segment.Play function, oFromPlayingSegment, which is the PlayingSegment object that this play event should play relative to. Don't forget that we still need to fill in the second and third parameters to the play function, for which we could use the keyword "nothing" or just leave blank (but still using commas to separate them).
Dim PlayingSegHello Dim PlayingSegMyNameIs Sub IntroduceCharacter Set PlayingSegHello = Hello.play (IsSecondary) Set PlayingSegMyNameIs = MyNameIs.play (AtFinish + IsSecondary, nothing, nothing, PlayingSegHello) Bob.play (AtFinish + IsSecondary, nothing, nothing, PlayingSegMyNameIs) End Sub
Script Tracks and Self-Modifying Music
One of the more interesting aspects of DirectX Audio Scripting is that script routines do not solely have to be triggered by the programmer. DirectMusic Segments themselves can fire off script routines at appropriate times, which allows for the concept of self-modifying music. A piece of music might play and check the state of a variable every four bars. If that variable has changed to a certain value, the music might stop of its own accord or fire off another piece of music, change tempo, or any of a number of actions.
Segments can trigger script routines by using a Script Track. When a Segment has a Script Track, you can place specific routine calls at any point in the Segment, and that routine will be called when the routine reaches that position. For instance, for our above example, a Segment might look like this:
Figure 7-12: Every four bars, the script routine CheckNumEnemies is called. If this Segment was our primary Segment, and the routine CheckNumEnemies fired off another primary Segment, this Segment would of course stop playing.
This can lead to all sorts of interesting implementations. Every piece of music in your application could fire off a script routine call right as it ends that figures out what piece of music to play next. So you could effectively have a self-running jukebox without the programmer having to code anything. Or the music can poll a variable every so often and respond accordingly without needing the programmer to let the script explicitly know that the variable has been updated. Such a technique is used in the baseball example included with the DirectX Software Development Kit — an infinitely looped secondary Segment consisting solely of a script track checks to see if the score has changed and whether the crowd has responded appropriately with cheers or boos (without needing the programmer's involvement in the least). See the baseball.spt file (which can be opened in DirectMusic Producer and re-expanded into its source files) for more details on how this was implemented.