Anatomy of a Script


DirectMusic's scripting support is all built around the Script object. The CLSID_DirectMusicScript object with its IDirectMusicScript interface represents the Script object. Like most DirectX Audio objects, the Loader reads the Script object from a file. Once it has loaded a script, the application can make direct calls into the script to run its routines, as well as pass variables back and forth between the script and the application.

So think of a script as a discrete object that is made up of:

  • Routines to call

  • Variables that store data as well as provide a means to pass the data between the application and the script

  • Embedded and/or linked content (Segments, waves, and any other DirectX Audio media) to be referenced by variables and manipulated by routines

Figure 12-1 shows an example of a script with its internal routines, variables, and content. The application manages the script and can make calls to trigger the routines and access the variables. In turn, the routines and variables access the embedded and linked Segments, AudioPaths, and other content.

click to expand
Figure 12-1: A script with its internal routines, variables, and content.

Routines

The heart of scripting is, of course, in the code. The code in a DirectMusic script is all stored in routines. There is no primary routine that is called first, unlike the required main() inaCprogram. Instead, any routine can be called in any order. Each routine exposes some specific functionality of the Script object. For example, two routines might be named EnterLevel or MonsterDies. The first would be called upon entering a level in a game. The second would be called when a monster dies. The beauty is that the programmer and content creator only need to agree on the names EnterLevel and MonsterDies and call them at the appropriate times.

Each routine has a unique name, and that name is used to invoke the routine via the IDirectMusicScript::CallRoutine() method.

 HRESULT CallRoutine(   WCHAR *pwszRoutineName,   DMUS_SCRIPT_ERRORINFO *pErrInfo ); 

Pass only two parameters — the name of the routine and an optional structure that is used to return errors. If the routine does indeed exist (which should always be the case in a debugged project), CallRoutine() invokes it immediately and does not return until the routine has completed.

Therefore, the code to call a routine is typically very simple:

 // Invoke the script handler when the princess eats the frog by accident pScript->CallRoutine(L" EatFrog",NULL);  // Yum yum. 

Notice that no parameters can be passed to a routine. If the caller would like to set some parameters for the routine, it must set them via global variables first. Likewise, if the routine needs to return something, that something needs to be stored in a variable and then retrieved by the caller. For example, a routine that creates an AudioPath from an AudioPath configuration might store the AudioPath in a variable, which the caller can then read immediately after calling the AudioPath creation routine.

So, let's look at variables.

Variables

There really are three uses for variables in a script:

  • Parameter passing variables: Declare a variable in the script and use it to pass information back and forth between the application and the script. An example might be a variable used to track the number of hopping objects in a scene, which in turn influences the musical intensity.

  • Internal variables: Declare a variable in the script and use it purely internal to the script. An example might be an integer that keeps track of how many times the princess steps on frogs before the fairy godmother really gets pissed.

  • Content referencing variables: Link or embed content (Segments, AudioPaths, etc.) in the script, and the script automatically represents each item as a variable. For example, if the Segment FrogCrunch.sgt is embedded in the script, the variable FrogCrunch is automatically allocated to provide access to the Segment.

Variables used in any of these three ways are equally visible to the application calling into the script.

Within the scripting language's internal implementation, all variables are handled by the variant data type. A variant is a shape-shifting type that can masquerade as anything from byte to pointer. To do so, it stores both the data and tag that indicates which data type it is. This is why you can create a variable via the "dim" keyword in a scripting or Basic language and then use it without first specifying its type. That is all fine and good in scripting, but it is not a natural way to work in C++. Therefore, IDirectMusicScript gives you three ways to work with variables and translates from variant appropriately:

  • Number: A 32-bit long, used for tracking numeric values like intensity, quantity, level, etc.

  • Object: An interface pointer to an object. Typically, this is used to pass objects back and forth with the application. In addition, all embedded and referenced content is handled as an object variable.

  • Variant: You still have the option to work with variants if you need to. This is useful for passing character strings and other types that cannot be translated into interfaces or longs. Fortunately, this is rare.

To accommodate all three types, IDirectMusicScript has Get and Set methods for each. They are GetVariableNumber(), GetVariableObject(), and GetVariableVariant() to retrieve a variable from the script and SetVariableNumber(), SetVariableObject(), and SetVariableVariant() to assign a value to a variable in the script.

Each Get or Set call passes the name of the variable along with the data itself. For example, the parameters for GetVariableNumber(), which retrieves a numeric value, are: the Unicode name, a pointer to a long to fill, and an optional error structure in case there's a failure.

 HRESULT // GetVariableNumber(   WCHAR *pwszVariableName,   LONG *plValue,   DMUS_SCRIPT_ERRORINFO *pErrInfo ); 

The code to retrieve a variable is simple:

 // Find out how many frog chances the princess has left long lFrogs; pScript->GetVariableNumber(L"FrogsLeft",&lFrogs,NULL); 

Likewise, setting a variable is straightforward. See the code snippet below using SetVariableObject() as an example:

 HRESULT SetVariableObject(   WCHAR* pwszVariableName,   IUnknown* punkValue,   DMUS_SCRIPT_ERRORINFO* pErrInfo ); 

Again, we pass the name of the variable. Since it is treated as an object, we pass its interface pointer (of which IUnknown is always the base). We can also pass that optional error structure (more on that in a bit).

 // Pass the script a 3D AudioPath for tracking a hopping frog IDirectMusicAudioPath *pPath = NULL; pPerformance->CreateStandardAudioPath(DMUS_APATH_DYNAMIC_3D,3,true,&pPath); if (pPath) {     pScript->SetVariableObject("FrogPath",pPath,NULL); } // Remember to release the path when done with it. The script will // release its pointer to the path on its own. 

Retrieving a variable object is a little more involved in that you need to identify what interface you are expecting. GetVariableObject() passes an additional parameter: the interface ID of the interface that it is expecting.

 // Retrieve a Segment that is stored in the script IDirectMusicSegment8 *pSegment; pScript->GetVariableObject(     "FrogScream",               // Name of the variable.     IID_IDirectMusicSegment8,   // IID of the interface.     (void **)&pSegment,         // Address of the interface.     NULL);                      // Optional error info. 

Content

Obviously, it is important that scripts be able to directly manipulate the DirectMusic objects that they control. To deal with this, key objects that you would want to manipulate from within a script all have scripting extensions. These are the AudioPath, AudioPath Configuration, Performance, Segment, and SegmentState (called a "playing Segment"). They all exhibit methods that can be called directly from within the script (internally, this is done via support for the IDispatch interface). Documentation for these scripting extensions can be found in the DirectMusic Scripting Reference section of the DirectMusic Producer help file, not the regular DirectX programming SDK.

It is also very important that a script be able to introduce its own content. It's not enough to load files externally and present them to the script with SetVariableObject() because that implies that the application knows about all of the objects needed to run the script, which gets us back to the content creator writing down a long list of instructions for the programmer, etc. Scripting should directly control which files are needed and when.

The Script object also supports linking and embedding objects at authoring time. A linked object simply references objects that are stored in files outside the script file. This is necessary to avoid redundancy if an object is shared by multiple scripts. If the script is the sole owner of an object, it can embed it directly, which results in a cleaner package because there are fewer files. Setting up linked or embedded content is very simple in DirectMusic Producer. In the project tree, just drag the objects you want to use in the script into its Embed Runtime or Reference Runtime folder. This also automatically makes sure the object loads with the script, and if the object can be directly manipulated, it appears as a variable in the script.

You can have everything you need for a particular section in your application all wrapped up in one script file, which is wonderfully clean. Be careful, though. By default, script files automatically download all their DLS and wave instruments to the synthesizer when the script is initialized. If you have more stuff than you want downloaded at any one time, you need to manage this directly in your scripting code and avoid the automatic downloading.

If you ever wondered why you could not simply open a script file in a text editor, the embedded and linked content is the reason. Although the code portion of the script is indeed text, the linked and embedded data objects are all binary in RIFF format, which cannot be altered in a text editor.

Finding the Routines and Variables

The IDirectMusicScript interface includes two methods, EnumRoutine() and EnumVariable(), which can be used to find out what is in the script. These are very useful for writing small applications that can scan through the script and display all routines and variables and then directly call them. Jones does that, as you will see in a bit.

 HRESULT EnumRoutine(   DWORD dwIndex,   // nth routine in the script.   WCHAR *pwszName // Returned Unicode name of the routine. ); 

EnumRoutine() simply iterates through all routines, returning their names.

 HRESULT EnumVariable(   DWORD dwIndex, // nth variable in the script.   WCHAR *pwszName // Return Unicode name of the variable. ); 

EnumVariable() does the same for each variable, but it's a little more involved because it enumerates all declared variables as well as all variables that were automatically created for linked and embedded objects. There is a little trick that you can use to figure out which type a variable is. Make a call to GetVariableLong(), and if it succeeds, the variable must be a declared variable; otherwise, it must be a linked or embedded object. Jones uses this technique.

You can also use GetVariableObject() to search for specific object types, since it requires a specific interface ID. Here is a routine that will scan a script looking for all objects that support IDirectMusicObject and display their name and type. This should display everything that is linked or embedded content and can be manipulated as a variable.

Note

Some content (for example, styles and DLS Collections) cannot be directly manipulated by scripting, so they do not have variables assigned to them.

 // Search for all content variables within a script // and display their name and type. void ScanForObjects(IDirectMusicScript *pScript) {     DWORD dwIndex;     HRESULT hr = S_OK;     // Enumerate through all variables     for (dwIndex = 0; hr == S_OK; dwIndex++)     {         WCHAR wzName[DMUS_MAX_NAME];         hr = pScript->EnumVariable(dwIndex,wzName);         // hr == S_FALSE when the list is finished.         if (hr == S_OK)         {             IDirectMusicObject *pObject;       // Only objects that can be loaded from file have the       // IDirectMusicObject interface.       HRESULT hrTest = pScript->GetVariableObject(wzName,           IID_IDirectMusicObject,           (void **) &pObject,           NULL);       if (SUCCEEDED(hrTest))       {           // Success. Get the name and type and display them.           DMUS_OBJECTDESC Desc;           Desc.dwSize = sizeof (Desc);           if (SUCCEEDED(pObject->GetDescriptor(&Desc)))           {               char szName[DMUS_MAX_NAME + 50];               if (Desc.dwValidData & DMUS_OBJ_NAME)               {                   wcstombs(szName,Desc.wszName,DMUS_MAX_NAME);               }               else               {                   strcpy(szName,"<unnamed>");               }               // This should be a Segment, AudioPath configuration,               // or another script because only these support IDispatch               // and can be loaded from file.               if (Desc.guidClass == CLSID_DirectMusicSegment)               {                   strcat(szName,": Segment");               }               else if (Desc.guidClass == CLSID_DirectMusicAudioPathConfig)               {                   strcat(szName,": AudioPath Config");               }               else if (Desc.guidClass == CLSID_DirectMusicScript)               {                   strcat(szName,": Script");               }               strcat(szName,"\n");               OutputDebugString(szName);            }            pObject->Release();          }       }    } } 

Error Handling

Sometimes tracking errors with scripting can be frustrating. Variable or routine names may be wrong, in which case calls to them fail. There can be errors in the routine code, but there is no way to step into the routines when debugging. Therefore, it helps to have a mechanism for figuring out what went wrong.

Enter the DMUS_SCRIPT_ERRORINFO structure. As we have seen already, CallRoutine() and all of the methods for manipulating variables can pass this as an option.

 typedef struct _DMUS_SCRIPT_ERRORINFO {     DWORD   dwSize;     HRESULT hr;     ULONG   ulLineNumber;     LONG    ichCharPosition;     WCHAR   wszSourceFile[DMUS_MAX_FILENAME];     WCHAR   wszSourceComponent[DMUS_MAX_FILENAME];     WCHAR   wszDescription[DMUS_MAX_FILENAME];     WCHAR   wszSourceLineText[DMUS_MAX_FILENAME]; } DMUS_SCRIPT_ERRORINFO; 

As you can see, this structure can provide a wealth of information to find out where something went wrong, especially if the error was in the script code. The SDK covers each of these fields quite well, so let's skip that. If you are curious, look in DirectMusic>DirectMusic C/C++ Reference>DirectMusic Structures at Microsoft's web site.

Script Tracks

You can also trigger the calling of script routines by using a Script Track in a Segment. This is very powerful because it gives the opportunity to have time-stamped scripting. You can even use scripting to seamlessly control the flow of music playback by calling routines at decision points in the Segments to decide what to play next based on current state variables.

Script Tracks are very straightforward to author and use. In DirectMusic Producer, open a Segment and use the Add Tracks command to add a Script Track to it. At the point in the timeline where you want the script routine called, insert a script event. Then choose from a pull-down menu in the Properties window which routine from which script to call. Each script event also has options for timing. The script routine can be called shortly ahead of the time stamp or exactly at it. The former is useful if you want to make decisions about what to play once the time stamp is reached. For example, you would not want to call the routine to queue a new Segment at exactly the time it should play, since latency would make that impossible.

The Script Track is quite flexible. It supports unlimited script calls. It allows calls to more than one script, and no two calls have to use the same timing options.

click to expand

Script Language

DirectMusic's scripting lets you choose between two language implementations. It does this by internally supporting a standard COM interface for managing scripted language, called IActiveScript. Theoretically, that means that you can use any language supported by IActiveScript, which includes a wide range of options from JavaScript to Perl. However, DirectMusic Producer offers content creators exactly two options, VBScript and AudioVBScript. This is just fine because they really are the best choices.

VBScript is a standard scripting implementation that Microsoft provides as part of the Windows operating system. VBScript is a very full-featured language. However, it is also big, requiring close to a megabyte to load.

AudioVBScript is a very light and nimble basic scripting language that was developed specifically for DirectMusic scripting. It is fast and very small and ships as part of the API. However, it does not have many of the more sophisticated features found in VBScript. Typically, scripters should use AudioVBScript. If the scripter runs up against a brick wall because AudioVBScript does not have a feature they need (like arrays, for example), then the scripter can simply switch to VBScript and continue. Since AudioVBScript is a subset of VBScript, this should be effortless.

Note

Documentation for the AudioVBScript language and all scripting commands are located in the DirectMusic Producer help files under Creating Content>Script Designer>AudioVBScript Language, not the regular DirectX help.




DirectX 9 Audio Exposed(c) Interactive Audio Development
DirectX 9 Audio Exposed: Interactive Audio Development
ISBN: 1556222882
EAN: 2147483647
Year: 2006
Pages: 170

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net