CHANGING TEMPO
One cool thing you can do with MIDI files (and not with MP3s or WAVs) is adjust the tempo on the fly. Tetris popularized the tempo change to create suspense; when your Play area was nearly full, the music would speed up, and when (or if!) you brought it under control again, the music would fall back to normal speed. This simple trick added a lot to the quality of the game, and you can use the same trick for your own titles.
Track versus Performance Tempo Settings
DirectMusic has two different ways you can change the tempo. You can change the tempo track contained in the segment, or you can disable that tempo track, and tell the performance to change the tempo of any segment Playing on it.This sounds weird, but I've found that the best course of action is to do both. You want to change the segment's tempo so that if you later query the segment for the tempo, it will report the actual tempo—not the tempo it was when it was first loaded. For example, if you load a MIDI file with a tempo of 109 beats per minute, and then you change the performance tempo to 200 beats per minute, when you query the segment for its tempo, it will return 109 (the segment's tempo really is 109—it just happens to be Playing at 200 thanks to the performance settings).You also want to set the performance tempo because you probably want all of the music that you're Playing to speed up when you change the tempo, not just that one segment.Fortunately, the code for both of these methods is pretty easy. Here's how CMIDIMusic::SetTempo looks:
void CMIDIMusic::SetTempo(int beats_per_minute)
{
// step 1: set the track tempo parameter
DMUS_TEMPO_PARAM Tempo;
Tempo.dblTempo = beats_per_minute;
ThrowIfFailed(
m_Segment->SetParam(GUID_TempoParam, 0xFFFFFFFF, 0, 0, &Tempo),
"CMIDIMusic::SetTempo: SetParam failed!"
);
// step 2: set the performance tempo
DMUS_TEMPO_PMSG* pTempo;
ThrowIfFailed(
m_Manager->GetPerformance()->AllocPMsg(
sizeof(DMUS_TEMPO_PMSG),
(DMUS_PMSG**)&pTempo),
"CMIDIMusic::SetTempo: couldn't allocate PMsg!"
);
// Cue the tempo event.
ZeroMemory(pTempo, sizeof(DMUS_TEMPO_PMSG));
pTempo->dwSize = sizeof(DMUS_TEMPO_PMSG);
pTempo->dblTempo = beats_per_minute;
pTempo->dwFlags = DMUS_PMSGF_REFTIME;
pTempo->dwType = DMUS_PMSGT_TEMPO;
ThrowIfFailed(
m_Manager->GetPerformance()->SendPMsg((DMUS_PMSG*)pTempo),
"couldn't send PMsg!"
);
}
This code is based on some sample code given in the DirectX documentation, at DirectX Audio\Using DirectX Audio\Playing Sounds\Timing\Changing the Tempo.The first step is to set the tempo track to the given value (beats_per_minute). The code does this by filling a DMUS_TEMPO_PARAM structure, and sending it to the track by calling the SetParam method of the segment interface. The DMUS_TEMPO_PARAM structure contains just one member, dblTempo, which is the new tempo you want to set.The second step is a bit more involved. First, the code must allocate a new PMsg, and store it in pTempo. Once it's allocated, the code fills in the structure. The most important member here is dblTempo, which should be set to the tempo you want to change the performance to.Note that there are a lot of members in this structure that got set to zero automatically when the code called ZeroMemory. The dwFlags member, however, can't be zero; it must be set to either DMUS_PMSGF_REFTIME or DMUS_PMSGF_MUSICTIME. It doesn't matter which one because the rtTime and mtTime members (not shown in this code) have both been set to zero thanks to ZeroMemory, so I just picked the one with less letters to type.Similarly, the dwType member also can't be zero. It has to be DMUS_PMSGT_TEMPO, so that DirectMusic knows it's getting a tempo event.Finally, once the structure is all set up, the code calls SendPMsg, which fires off the pTempo message and causes DirectMusic to change the tempo.
Getting the Tempo
Getting the tempo is much easier than setting it. To get the tempo, just pass a DMUS_TEMPO_PARAM structure to the GetParam method of the segment, like so:
int CMIDIMusic::GetTempo()
{
DMUS_TEMPO_PARAM Tempo;
m_Segment->GetParam(GUID_TempoParam, 0xFFFFFFFF, 0, 0, NULL, &Tempo);
return(Tempo.dblTempo);
}
Nothing to it! Give GetParam the GUID of the parameter you want to receive (GUID_TempoParam), and a structure it should put it in (Tempo), and the requested information will magically appear! Don't worry about the other parameters between GUID_TempoParam and Tempo—they're not usually used. If you want to know what they are, look up GetParam in the DirectX documentation.
The Tempo Scaling Factor
DirectMusic has a second tempo control, called the master tempo, that you also need to be aware of. The master tempo isn't really a tempo so much as it is a scaling factor for the performance's tempo. It is one by default, but you can change it to values between zero and one to slow the performance down, or you can raise it to values above one to speed things up. The DMUS_MASTERTEMPO_MIN and DMUS_MASTERTEMPO_MAX constants define the minimum and maximum limits.The master tempo is very handy for doing Tetris-style music speedups because you don't need to know the original tempo to speed up a song. All you do is raise the master tempo above one to speed things up, and then set it back to one to restore normal tempo. It's sort of like a virtual scrub bar!Getting and setting the value of the master tempo is very easy. I've added a couple of new methods to CAudioManager that accomplish this. First, here's SetMasterTempoScaleFactor:
void CAudioManager::SetMasterTempoScaleFactor(float value)
{
if (NULL == m_Performance) return;
ThrowIfFailed(m_Performance->SetGlobalParam(GUID_PerfMasterTempo, &value,
sizeof(value)),
"CAudioManager::SetMasterTempo: SetGlobalParam failed.");
}
The pattern is identical to what you used to set the master volume—just call SetGlobalParam, passing in the GUID of the thing you want to set, along with the new value and the size in bytes of the new value. Nothing to it!Next up is GetMasterTempoScaleFactor:
float CAudioManager::GetMasterTempoScaleFactor()
{
if (NULL == m_Performance) return(0.0f);
float tempo=0.0f;
ThrowIfFailed(m_Performance->GetGlobalParam(GUID_PerfMasterTempo, &tempo,
sizeof(tempo)),
"CAudioManager::GetMasterTempo: GetGlobalParam failed.");
return(tempo);
}
This is similarly easy—call GetGlobalParam, supplying the GUID of the parameter you want to query, along with a variable to store the value in, and the size of that variable, and DirectMusic will gladly give you back any global parameter you desire.These are a couple of simple methods that come in very handy when you don't care what the tempo of a song is but you just want to speed it up.