Vic, this is the way I achieved it on my DBP entry.
I created loopable pieces of music that could be seamessly interchanged and played continuosly if desired. These little loops were several bars long, some were 2, others were 4 bars. Each bar was recorded using a known beat per minute using a metronome of course, for example 130 BPM or 90 BPM.
It is good game programming practice to make your game go with the rythm of the computer time, so all movement will take place according to the elapsed game time. On Music based games, you go with the rythm of the music, and if you think of it, it is still a time based programming approach.
To avoid the problem that you described, what I did was to manually play the cues for the next music loop, move everything according to the elapsed time and the estimated duration of a single loop sample, then when the time to play a new loop arrived, sync everything again, so each time a loop is played everything else gets in sync.
Here is part of a class I called Virtual Orchestra in my game. It updates a public property called isNewMeasure . Also note the method PlayedPercent, which I use to calculate the position of the visual elements according to the estimated music time.
//.NET Framework 2.0
using System;
using System.Collections.Generic;
//XNA
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
namespace BabyGamer_Musical_Rain
{
public class VirtualOrchestra
{
private MyGame game;
public int BPM = 90; //Beats per second
public int numMeasures = 8; //Number of measures per wave
public TimeSpan musicTimeSpan = TimeSpan.Zero; //Hold the elapsed time since last Play
public TimeSpan lastMusicTimeSpan = TimeSpan.Zero; //Hold the elapsed time since of the last loop, for debugging
public bool isNewMeasure = false;
public VirtualOrchestra(MyGame game)
{
this.game = game;
}
public void Update(GameTime gameTime)
{
musicTimeSpan += gameTime.ElapsedGameTime;
//Check if we are in a new music loop
if (musicTimeSpan.Add(TimeSpan.FromMilliseconds(StimatedPlayCueDelay())) > TimeSpan.FromMilliseconds(60000 * numMeasures / BPM))
//if (musicTimeSpan > TimeSpan.FromMilliseconds(60000 * numMeasures / BPM))
{
isNewMeasure = true;
//Reset TimeSpan
lastMusicTimeSpan = musicTimeSpan;
musicTimeSpan = TimeSpan.Zero;
}
else
{
isNewMeasure = false;
}
}
public void PlayNextMeasure()
{
//Play cue
game.soundBank.PlayCue("YourLoop");
}
public float PlayedPercent
{
get { return (float)(musicTimeSpan.TotalMilliseconds / (60000 * numMeasures / BPM)); }
}
}
}
Put something similir to this in your main update method. Try to do it as early as you can, and leave all the other logic take place after you have played the loop.
virtualOrchestra.Update(gameTime);
if (virtualOrchestra.isNewMeasure)
{
//Play Next measure. Must be first instruction of the update functions beacause speed is needed
virtualOrchestra.PlayNextMeasure();
}
Finally a sample of how to do calculations based on the music progress.
fi.Y = (float)(fallingHeight - fallingHeight * virtualOrchestra.PlayedPercent);
fi.rotation = MathHelper.TwoPi * virtualOrchestra.PlayedPercent;
As I stated, this worked for me nicely. In my game musical instruments were falling from the sky and the music was playing accourdingly to the instruments falling and instruments previously caught; you may think of an analogy to Guitar Hero falling notes and previously caught notes. Each instrument took exactly one loop to go from sky to floor, and they rotated exactly 360 degrees (TwoPi) per loop.