Chanel Summers
Microsoft Corporation
January 12, 2000
Summary: This article answers the most frequently asked questions about the Microsoft DirectMusic API and is based on Microsoft DirectX, version 7.0. (21 printed pages)
General DirectMusic Issues
Developer/Composer Communication Issues
Programming 3-D Music
Miscellaneous Issues
Messages and Tools
Notifications
IDirectMusicTrack and Patterns
The Composition Engine
Troubleshooting Issues
Table 1. CPU usage regarding reverb and sampling rate
Reverb Status | Sampling Rate | CPU Usage |
Reverb off | 22 kHz | Least CPU |
Reverb on | 22 kHz | Better sounding |
Reverb off | 44.1 kHz | Probably not that useful; 22 kHz with reverb on usually sounds better, but use your own taste |
Reverb on | 44.1 kHz | Best sounding if you are using 44.1-kHz samples |
You could also give the user ultimate control through the audio control panel in your game. Of course, if all your samples are 22kHz, you should run the synthesizer no faster than 22 kHz.
[debug]
DMBAND=-1
This can be used to turn off debug statements and is sometimes necessary because debug statements can affect performance for some DLLs more than others.
If you’re working on a typical game application, you probably don’t want to use AutoDownload, as it can cause a performance hit when playing back Segments. Instead, manually download with Segment->SetParam( GUID_Download, pIPerformance) to tell the Segment to download the DLS instruments associated with the Segment. This should be called at a convenient time (like a scene change) or you can call it in a separate thread prior to playback. The Band should be placed in a Band Track in your Segment to ensure that this will work properly.
After playing the Segment, call Segment->SetParam(GUID_Unload, pIPerformance) when you’re done with the Segment. For this to work, all collections must be referenced properly from within the Band. When you load the Segment, the Band Track reads the name, file name, and GUID for each referenced collection and asks the Loader to load those as well. The easiest way for the Loader to know where to find them is to rely on file names. If you store your data in a resource, then you should call SetObject on each resource chunk first so the Loader will know where to find it.
When using AutoDownload, if you are using only the instruments from the default collection (GM.DLS) in your primary Segment and the Band in your Secondary Segment references only instruments from a custom collection (replacing GM.DLS), then the instruments from the default collection should be returned automatically when the Secondary Segment playback stops, if the primary Segment is still playing.
If you write a basic Play Segment/MIDI file app, you can use AutoDownload so you don’t have to manage downloading the instruments. However, in a typical game situation, AutoDownload incurs a performance hit if you ever play a Segment more than once. It also causes the downloading of instruments to occur right at the start of Segment playback, causing a blip in CPU at that point and potential delay in performance. Downloading and unloading repeatedly (which AutoDownload may do) takes time, and can potentially degrade performance. If you are concerned about CPU performance in your application, consider turning AutoDownload off.
Relying on AutoDownload can cause other problems. You might also want to turn off AutoDownload if you have a Band in a Secondary Segment (Secondary Segments are played on top of a primary Segment). Otherwise the instruments in the Band may be downloaded automatically when the Secondary Segment starts, changing your instruments. If the Secondary Segment stops playing before the primary Segment stops, AutoDownload will then unload the Band. If this happens, you will not revert to the original instruments, as you may expect. Rather, you may lose sound output entirely because you now have no Bands loaded.
Further information on downloading custom collections can be found under What is the best way to download a custom collection?
Yes. EAX can be supported with our software synthesizer the same way that three-dimensional (3-D) audio is. You create your own DirectSound buffer, pass it to the DirectMusic software synthesizer, and then you can manipulate the DirectSound buffer the same way you would any DirectSound buffer. There’s an example of doing 3-D sound on the DirectX SDK, but the same concepts can apply to anything that DirectSound can do, including EAX.
After you get the reverb settings from the composer, you can set the reverb manually using KSControl. Please note that the composer may specify different reverb settings per Segment.
DMUS_WAVES_REVERB_PARAMS Params;
Params.fInGain = m_fReverbIn;
Params.fHighFreqRTRatio = m_fReverbHigh;
Params.fReverbMix = m_fReverbMix;
Params.fReverbTime = m_fReverbTime;
IKsControl *pControl;
// Query for IKsControl interface
HRESULT hr = m_pPort->QueryInterface(IID_IKsControl,
(void**)&pControl);
if (SUCCEEDED(hr))
{
KSPROPERTY ksp;
ULONG cb;
ZeroMemory(&ksp, sizeof(ksp));
ksp.Set = GUID_DMUS_PROP_WavesReverb;
ksp.Id = 0;
ksp.Flags = KSPROPERTY_TYPE_SET;
pControl->KsProperty(&ksp, sizeof(ksp),
(LPVOID)&Params, sizeof(Params), &cb);
pControl->Release();
Here is a checklist of suggestions/caveats/gotchas to be aware of:
Example: If you have a segment that has notes on PChannels 1, 4, 5, 33, and 59, you’ll need to at least do the following:
pPerf->AssignPChannelBlock(0, pPort, 1); // for channels 1, 4, and 5
pPerf->AssignPChannelBlock(1, pPort, 2); // for channel 33
pPerf->AssignPChannelBlock(3, pPort, 3); // for channel 59
(Channel group is arbitrary, but must be 1 or greater, and not collide with other group assignments.)
Here is the workaround:
HRESULT CacheDefaultGMCollection
(
IDirectMusicLoader* pLoader
)
{
HRESULT hr = E_FAIL;
DMUS_OBJECTDESC desc;
static IDirectMusicCollection* pCollection = NULL;
if (NULL != pCollection)
{
pCollection->Release();
pCollection = NULL;
}
//**********************************************************************
// Setup DMUS_OBJECTDESC to represent Default GM Collection
//**********************************************************************
ZeroMemory(&desc, sizeof(desc));
desc.dwSize = sizeof(DMUS_OBJECTDESC);
desc.guidObject = GUID_DefaultGMCollection;
desc.guidClass = CLSID_DirectMusicCollection;
desc.dwValidData = (DMUS_OBJ_CLASS | DMUS_OBJ_OBJECT);
hr = pLoader->GetObject(&desc, IID_IDirectMusicCollection, (void **)&pCollection);
if ( FAILED(hr) )
{
DPF(0, “**** Failed to Load Object [collection]”);
}
return hr;
}
The mere fact of doing a GetObject with the above DMUS_OBJECTDESC will re-establish the Loader’s link to the Default GM Collection. Also, you do not need to release the Collection; it will be released when the Loader goes away.
Note This is one possible implementation of this workaround. A more solid solution could include wrapping the check for Collection pointer with a critical section, to prevent synchronization problems.
[debug]
DMBAND=3
Use Bands to reference the instruments in the collection. If you want to use the Band explicitly, call Band->Download(pIDirectMusicPerformance) to cause the instruments selected by the Band to be downloaded. Later, call Band->Unload.
Even better, the Band should be placed in a Band Track in your Segment. There, it sets the volume, pan, priorities, and instruments for the channels. Before playing the segment, call Segment->SetParam( GUID_Download, pIPerformance) to tell the Segment to download the DLS instruments associated with the Segment. This should be called at a convenient time (like a scene change) or you can call in a separate thread. After playing the Segment call Segment->SetParam( GUID_Unload, pIPerformance) when you’re done with the Segment.
For this to work, all collections must be referenced properly from within the Band. When you load the Segment, the Band Track reads the name, file name, and GUID for each referenced collection and asks the Loader to load that as well. So, it’s important that the Loader know where to find these. The easiest is to rely on file names. If you store your data in a resource, then you should call SetObject on each resource chunk first so the Loader will know where to find it.
No, you can have any number of collections active at the same time. A Band can reference collections in addition to the GM collection (which doesn’t require a specific linkage).
However, you can override the GM collection, if you want to.
Here’s a function to set up the Loader to find an object from memory in response to a later GetObject call from the application, or an internal reference from another object that is being loaded by the application. This is particularly important for having Segments reference DLS collections. But, it also applies to Segments referencing Styles.
The function passes a chunk of memory and the object class of the object.
HRESULT SetMemoryObject(IDirectMusicLoader *pLoader,BYTE *pData,DWORD dwLength,GUID guidClassID)
{
DMUS_OBJECTDESC ObjDesc;
ObjDesc.pbMemData = pData;
ObjDesc.llMemLength = (LONGLONG) dwLength;
ObjDesc.guidClass = guidClassID;
ObjDesc.dwSize = sizeof(DMUS_OBJECTDESC);
ObjDesc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_MEMORY;
return pLoader->SetObject( &ObjDesc);
}
Here’s a function that does the same thing with a resource ID:
HRESULT SetResourceObject(IDirectMusicLoader *pLoader,UINT uiResourceID,GUID guidClassID)
{
DMUS_OBJECTDESC ObjDesc;
HRSRC hFound = FindResource(NULL,MAKEINTRESOURCE(uiResourceID), RT_RCDATA);
ObjDesc.llMemLength = SizeofResource(NULL,hFound);
HGLOBAL hRes = LoadResource(NULL, hFound);
ObjDesc.pbMemData = (BYTE *) LockResource(hRes);
ObjDesc.guidClass = guidClassID;
ObjDesc.dwSize = sizeof(DMUS_OBJECTDESC);
ObjDesc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_MEMORY;
return m_pLoader->SetObject( &ObjDesc);
}
Once the objects have been set in the Loader, it’s very easy to access them by calling to GetObject.
And now, the Segment downloading code, which you can call right after you load the Segment:
pSegment->SetParam(GUID_Download,-1,0,0,(void *) pPerformance);
and the Segment unloading code, which you can call just before you release the Segment:
pSegment->SetParam(GUID_Unload,-1,0,0,(void *) pPerformance);
These errors are occurring because the DLS instruments have not been downloaded. You need to turn off AutoDownload and manually download your Bands. 0,0,0 is the default value for patch selection in a synthesizer when no program change/bank select has been received. So, if you are seeing this message, it would imply that no program changes or bank selects are making it to the synthesizer on the channel. The notes arrive and are told to play the default, 0,0,0, but no instrument with the address 0,0,0 was ever downloaded.
Remember, this error message is caused by no program change on the channel that selects a currently downloaded instrument. So, three things can cause it:
Additionally, you could get this error if a range of the instrument was downloaded and you sent patch changes, but are playing notes out of the downloaded range. This can also happen when using the default collection (GM.DLS) and you are playing drums but there is no drum instrument assigned to a particular MIDI value in the collection. The Standard Drums in GM.DLS has an extended range from D#2 (value 27) to D#7 (value 87), but the GM Standard for drums is B2 to D#7 (values 36 to 87). This can easily happen with converted MIDI files that have be created using external drum modules as the sound source.
Also, this debug output is the most likely one you will see that will make it necessary to turn debugging statements off. There will be one debug statement per MIDI note, which can add up to a lot of output.
Since both DirectMusic software and hardware synthesizers use system RAM (as opposed to on-board RAM), it is not very likely that you will run out of RAM. But in the unlikely case that you do, you may receive E_OUTOFMEMORY or DMUS_S_PARTIALDOWNLOAD, depending on method of download.
This function will play a Segment directly from a resource, assuming all the right DirectMusic setup has been done.
You can also call IDirectMusicLoader::SetObject() and later use IDirectMusicLoader::GetObject() to load it, just as you would if the segment was on disk. SetObject puts the Segment in the Loader list as if it was found by a call to ScanDirectory.
//==================================================================
HRESULT PlaySegmentFromResource(int ID,char* type)
{
HRESULT hr = E_FAIL;
DMUS_OBJECTDESC desc;
IDirectMusicSegment* pSegment = NULL;
HRSRC hResource = NULL;
HGLOBAL hData = NULL;
//must have a properly setup loader and a performance
if(m_pLoader && m_pPerformance)
{
hResource =
FindResource(GetModuleHandle(NULL),MAKEINTRESOURCE(ID),type);
if(hResource != NULL)
{
hData = LoadResource(GetModuleHandle(NULL),hResource);
if (hData != NULL)
{
ZeroMemory(&desc,sizeof(desc));
desc.pbMemData = (BYTE*)LockResource(hData);
desc.llMemLength = SizeofResource(GetModuleHandle(NULL),hResource);
desc.guidClass = CLSID_DirectMusicSegment;
desc.dwSize = sizeof(desc);
desc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_MEMORY;
hr = m_pLoader-
>GetObject(&desc,IID_IDirectMusicSegment,(void**)&pSegment);
if(SUCCEEDED(hr))
{
hr = m_pPerformance->PlaySegment(pSegment,0,0,NULL);
pSegment->Release();
pSegment = NULL;
}
}
}
}
return hr;
}
//=================================================================
//=================================================================
HRESULT SetSegmentObjectFromResource(int ID,char* type)
{
HRESULT hr = E_FAIL;
DMUS_OBJECTDESC desc;
IDirectMusicSegment* pSegment = NULL;
HRSRC hResource = NULL;
HGLOBAL hData = NULL;
//must have a loader and a performance
if(m_pLoader && m_pPerformance)
{
hResource =
FindResource(GetModuleHandle(NULL),MAKEINTRESOURCE(ID),type);
if(hResource != NULL)
{
hData = LoadResource(GetModuleHandle(NULL),hResource);
if (hData != NULL)
{
ZeroMemory(&desc,sizeof(desc));
desc.pbMemData = (BYTE*)LockResource(*hData);
desc.llMemLength =
SizeofResource(GetModuleHandle(NULL),(*hResource));
desc.guidClass = (*guid);
desc.dwSize = sizeof(desc);
desc.dwValidData = DMUS_OBJ_CLASS | DMUS_OBJ_MEMORY;
return m_pLoader->SetObject(&desc);
}
}
}
return hr;
}
//===================================================================
Verify that the IDirectMusicPort supports DirectSound by checking port caps:
DMUS_PORTCAPS.dwFlags & DMUS_PC_DIRECTSOUND
Create IDirectSound, then pass to DirectMusic:
pDMusic->SetDirectSound(pDSound, hWnd)
Query required wave format and length:
pPort->GetFormat(&WFormat,&dwWFXSize,&dwBufferSize)
Create IDirectSoundBuffer using format and length
Set Port to render into DirectSound buffer:
pPort->SetDirectSound(pDSound, pDSBuffer)
Activate Port
pPort->Activate( TRUE )
There is a sample in the SDK called, “3DMusic.” It shows how to play a MIDI file into a custom DirectSound 3D buffer. The default location is C:\mssdk\samples\multimedia\dmusic\src\3dmusic.
For separate tempos, yes. The overhead of having multiple performances is perfectly fine. They are effectively separate pieces of music, which is why we introduced the concept of multiple performances.
Global volume on the Performance really is control over the volume of the port. So, the problem there is that you would need multiple ports. You could also programmatically send volume or CC11 (Expression) controls on a channel by channel basis, or use Bands.
Call IDirectMusicPerformance::SetGlobalParam() method. This sets the global value for the Performance. Call IDirectMusicPerformance::GetGlobalParam() method. This retrieves the global value from the Performance. Each Global Parameter is defined by a GUID and data structure, for example: GUID_PerfMasterVolume, (long) lVolume.
The master volume is an amplification or attenuation factor, in hundredths of decibels, applied to the default volume of the entire performance. The range of permitted values is determined by the port. Legacy hardware MIDI ports do not support changing master volume.
You can send MIDI expression events for each PChannel in the Segment. Even better, send curves, which can have expression start and end values as well as curve shape. You can also write an IDirectMusicTool that sits in the Performance and monitors volume and expression events as they pass through it. This Tool also has a method that the application uses to control the overall volume through a multiplier that is applied to the volume and expression events as they flow through the Tool.
In both cases, you need to know which PChannels belong to which Segments. This can be avoided with the Tool solution if the Tool is placed in the Segment’s Tool Graph.
Yes. You create a Secondary Segment with volume controller curves for each of your PChannels that fade to zero over the length of the time you want the fade to occur. You would then play the Secondary Segment when you want to do the fadeout and voila—instant fadeout.
They include chord, time signature, Groove Level, and mute control. It is also extensible so you can add new controls. Control Parameters do not include tempo, volume, instrument patch, pan, or any other performance information that is communicated by a Performance Message.
You can call GetParam to get the chord and read it.
// declare the chord
DMUS_CHORD_KEY Chord;
// Call GetParam with the time in lTime.
pPerformance->GetParam(GUID_ChordParam,-1,0,lTime,NULL,&Chord);
// Now, pull the key and scale. The root of the key is in bKey. The
scale is a bit pattern, stored in dwScale.
// The bit pattern starts at position 0 for the first note of the scale
and moves up fom there.
// Each set bit represents a semitone that is on, a member of the
scale. There should be 7 set bits per
// octave (every twelve bits).
Here’s the structure:
typedef struct _DMUS_CHORD_KEY
{
WCHAR wszName[16]; /* Name of the chord */
WORD wMeasure; /* Measure this falls on */
BYTE bBeat; /* Beat this falls on */
BYTE bSubChordCount; /* Number of chords in the list
of subchords */
DMUS_SUBCHORD SubChordList[DMUS_MAXSUBCHORD]; /* List of sub
chords */
DWORD dwScale; /* Scale underlying the entire
chord */
BYTE bKey; /* Key underlying the entire
chord */
} DMUS_CHORD_KEY;
Try it and write some code to print out the values and see experimentally what happens when different chords and keys are defined in DirectMusic Producer.
To change the key, there are several techniques:
Here’s some code that will do this:
// Code to transpose all chords in a Segment’s Chord Track.
// Transpose chord or key root by a given amount, clamped to a two-
octave range.
inline void AddAndClamp(BYTE& rbRoot, int nAmount)
{
if (nAmount < -24 || nAmount > 24) return; // only allow 2-octave
transposition
char chRoot = (char) rbRoot;
chRoot += nAmount;
while (chRoot < 0) chRoot += 12;
while (chRoot > 23) chRoot -= 12;
rbRoot = (BYTE) chRoot;
}
// Transpose all chords in a Segment’s Chord Track by a given amount.
// NOTE: this function assumes that the first chord occurs at time 0 in
the Chord Track.
// If it does not, the time of the first chord (use in the call to
SetParam) needs to be
// computed from its measure and beat, along with time signature
information from the Segment.
HRESULT Transpose(int nAmount, IDirectMusicPerformance* pPerformance,
IDirectMusicSegment* pSegment)
{
if (!pPerformance || !pSegment) return E_INVALIDARG;
HRESULT hr = S_OK;
DMUS_CHORD_KEY ChordKey;
MUSIC_TIME mtChord = 0;
MUSIC_TIME mtNextChord = 0;
MUSIC_TIME mtSegmentLength = 0;
// Get the length of the Segment
hr = pSegment->GetLength(&mtSegmentLength);
if (S_OK != hr) return hr;
do
{
// Get the next chord.
hr = pSegment->GetParam(GUID_ChordParam, 0xffffffff, 0, mtChord,
&mtNextChord, &ChordKey);
if (S_OK != hr) return hr;
if (ChordKey.bSubChordCount <= 0) return E_FAIL;
// Change the chord.
AddAndClamp(ChordKey.bKey, nAmount);
for(int i = 0; i < ChordKey.bSubChordCount; i++)
{
AddAndClamp(ChordKey.SubChordList[i].bChordRoot, nAmount);
AddAndClamp(ChordKey.SubChordList[i].bScaleRoot, nAmount);
}
// Plunk the chord back in.
hr = pSegment->SetParam(GUID_ChordParam, 0xffffffff, 0, mtChord,
&ChordKey);
if (S_OK != hr) return hr;
mtChord += mtNextChord;
} while (mtChord < mtSegmentLength);
return hr;
}
Write a Tool that intercepts the notes, then passes them on. The Tool can run DMUS_PMSGF_TOOL_IMMEDIATE, in which case it gets all events half a second before they are heard.
The way to send your own data is to create a COM interface for the data with, at minimum, just IUnknown on it. Then, you can handle memory allocation and freeing yourself. If the message is deleted out of the blue, it can safely do so because it calls Release() on the interface.
Tools are tiny pieces of code that intercept music Messages (including MIDI data) after they are generated by Segments. Tools can modify, add, or remove these Messages (called “PMsg”s) before they are passed on to the port. Tools are accessed through the IDirectMusicTool interface. Tools can read the current controlling track information through calls to GetParam():
Some example Tools include:
There is a sample in the SDK called, “EchoTool,” which shows how to implement a Tool in DirectMusic. The default location is C:\mssdk\samples\multimedia\dmusic\src\EchoTool.
About half a second before the Segment finishes playing (or, more precisely, Prepare Time away from end). This gives you the chance to queue up another Segment to replace it. Note that this Message is sent for both primary and Secondary Segments, so you need to check the SegmentState that comes with it. You can Get and Set the PrepareTime by calling the appropriate Performance methods (GetPrepareTime, SetPrepareTime).
If a Segment is not interrupted, you should receive SEGSTART, then SEGALMOSTEND (at the time described above), then SEGEND. If you are currently playing a primary Segment and play another primary Segment at QUEUE time, the current primary Segment will not receive a SEGEND notification. A Segment may be interrupted by a call to IDirectMusicPerformance->Stop(), or the primary Segment may be interrupted by calling IDirectMusicPerformance->PlaySegment() to start another primary Segment before the current primary Segment ends. In these cases, a SEGABORT notification will be sent when the interrupted Segment stops. SEGALMOSTEND will not be sent for it.
Correct, Motif Tracks are created “on the fly” by DirectMusic when you call the Style->GetMotif() API. This creates a Segment with a Motif Track in it. There is no way to stick a Motif Track into a Segment in DirectMusic Producer because there’s no file format for saving Motif Tracks. We have several features coming in a future version of DirectX that address this in different ways.
No, there is no way to access the Parts in a Pattern in a Style, other than by actually parsing the Style yourself. You can create the representation of the Style (with any Parts or Patterns that you put in it) as a file and dynamically stream it into the Style. The easiest way to do this is just to write the whole thing into a chunk of memory, point the Loader to it with SetObject(), then call GetObject() to create a new Style with it. If you continue by using the same object, be sure to ReleaseObject() before reloading the Style or the Loader will simply return the previous object.
No, because Patterns are not composed of Tracks. They are composed of Parts, which are internal to Patterns. Tracks, on the other hand, are plug-ins that can be placed within Segments, but only Segments.
Sorry, it’s a fixed file format. There’s no plug-in technology within a Style.
A Template is a special type of a Segment. It is designed for Style playback, but without a chord progression. It carries the “roadmap” for building a Style playback Segment. It is used in conjunction with a Style and Chordmap to compose music at run-time. Unlike other Segments, a Template Segment is never played directly by an application; instead, it is passed to the Composer object to be used in creating a musical Segment.
There are two methods for composing transitional Segments:
Both these methods take a Chordmap, a command, and a set of flags as parameters.
A Shape is a predefined pattern of intensity. It uses predefined Groove Level behavior to create a Segment that serves a specific musical purpose. Some types of Shapes are:
IDirectMusicComposer::ComposeSegmentfromTemplate() uses a Template Segment and a Chordmap to build a new Style Segment. IDirectMusicComposer:: ComposeSegmentfromTemplate() uses a Shape, Style, and Chordmap to build a new Style Segment.
ComposeSegmentfromTemplate() gives you a lot more control than ComposeSegmentfromShape(). It gives you control over where chords start and stop. But it assumes that your Template is a specific length. With ComposeSegmentfromShape(), you don’t start with a Segment, you build it. You can also have Segments with different lengths. Basically, you can have lots of different kinds of behaviors without having to write different Templates.
This could be happening if you are running debug bits. If you have debug bits and the debug level is turned up, loading can indeed take a long time.
Invalidating the Performance is not good practice. The invalidation is probably overloading the Microsoft synthesizer’s event queue.
You received DMUS_E_DSOUND_NOT_SET because the port could not be created, because no DirectSound object has been specified. On non-accelerated systems, DirectMusic uses DirectSound for output. If your application uses both DirectSound and DirectMusic—when you initialize your Performance, you should pass the DirectSound object you have previously created. If a NULL parameter is used, DirectMusic will create its own DirectSound object and this can cause problems. So, DO this:
// Init the performance using the DirectSound object
hr = g_pPerformance->Init( &pDM, pDSound, hWnd );
Not this:
// Init the performance using the NULL object
hr = g_pPerformance->Init( &pDM, NULL, NULL );
DMUS_COMMAND_PARAM GrooveCommand;
GrooveCommand.bCommand=DMUS_COMMANDT_GROOVE;
GrooveCommand.bGrooveLevel=100;
GrooveCommand.bGrooveRange=5;
HRESULT hr=m_pSegment->SetParam(GUID_CommandParam, 0xFFFFFFFF, 0, 0,(void
*)(&GrooveCommand) );
This command actually changes the value in the Command Track of the Segment; it doesn’t issue a “change the Groove Level now” command. To do that, you need to use:
char gl;
Performance->SetGlobalParam(GUID_PerfMasterGrooveLevel, &gl, sizeof(gl));
Where gl = desired Groove Level offset.
Make sure your composer has a Groove Level Track in his or her Segment. Have the composer set the Groove Level to some value that you decide upon.
Important Note The Groove Level you set is relative to the Groove Level set by the composer, not an absolute setting. So, if the segment was created with a Groove Level of 30 and you use the above with gl = 20, then Patterns with Groove Level 50 will play.