So here’s a nice way to get an acceptably precise metronome, with custom BPM and signature. The purpose is to create a MonoBehavior that you can stick to an entity to count your beats. Let’s start by creating a new C# script (javascript should be straight forward, but I don’t use it, sorry). I named it metronome, because that’s what it is. We’ll add in a few fields that will make sense soon enough :
public int Base;
public int Step;
public float BPM;
public int CurrentStep = 1;
public int CurrentMeasure;
private float interval;
private float nextTime;
First 3 fields should be straightforward if you know a little bit of music theory : signature represented by its base and step amount, and Beats Per Minute. CurrentStep
and CurrentMeasure
just let us keep track of what step/measure we're on. Now this is where the trick starts in order to be as precise as possible : is the absolute amount of seconds there should be between 2 beats. is the relative moment of when the beat will actually occur.
I see the guys in the back of the room going all “WTF man”, but it’ll make sense in a second : we are going to use Unity Coroutines. Unity uses Mono, which supports a great load of C# thread operations, including Tasks. The problem is, Unity is not thread safe, and tends to go all “NOPENOPENOPE” when you use threads and Unity objects. This is were the coroutines come in : they are a sort of bastard “multitask” technique that consist in watering down heavy operations between frames. That’s the important word : frame. Basically, a coroutine is a method that disperses its executions on a frame to frame basis. On its simplest form, it works much like a simple component update, as launched coroutines are each called by default once per frame until they are over. The interesting part is the kind of value you can send to your .
UnityGems did a great article on coroutines, and this is what you can send as a return value (shameless copy from their article) :
- null — the coroutine executes the next time that it is eligible
- WaitForEndOfFrame — the coroutine executes on the frame, after all of the rendering and GUI is complete
- WaitForFixedUpdate — causes this coroutine to execute at the next physics step, after all physics is calculated
- WaitForSeconds — causes the coroutine not to execute for a given game time period
- WWW — waits for a web request to complete (resumes as if WaitForSeconds or null)
- Another coroutine — in which case the new coroutine will run to completion before the yielder is resumed
I highlighted the one we are going to enjoy very much : WaitForSeconds. It’s straightforward, give it an amount of time and it will execute when that time is consumed. Let’s write a first coroutine with that!
IEnumerator DoTick() // yield methods return IEnumerator
{
for (; ; )
{
Debug.Log("bop");
// do something with this beat
yield return new WaitForSeconds(interval); // wait interval seconds before next beat
CurrentStep++;
if (CurrentStep > Step)
{
CurrentStep = 1;
CurrentMeasure++;
}
}
}
Simple coroutine : infinite loop that increments CurrentStep
and CurrentMeasure
. It works pretty fine, but the more discerning readers will have noticed that we never set interval. I'm going to do a simple public method for that, to be able to reset and change my coroutine :
public void StartMetronome()
{
StopCoroutine("DoTick"); // stop any existing coroutine of the metronome
CurrentStep = 1; // start at first step of new measure
var multiplier = Base / 4f; // base time division in music is the quarter note, which is signature base 4, so we get a multiplier based on that
var tmpInterval = 60f / BPM; // this is a basic inverse proportion operation where 60BPM at signature base 4 is 1 second/beat so x BPM is ((60 * 1 ) / x) seconds/beat
interval = tmpInterval / multiplier; // final interval is modified by multiplier
StartCoroutine("DoTick"); // start the fun
}
This goes a bit more into music theory, but I suppose you can deal with that if you’ve read this far. So we get the absolute interval between each beat and store it in our field. You’ll notice I use StartCoroutine
and StopCoroutine
with a string of the coroutine method name. This method is more expensive but allows us to stop the coroutine at will, which is appreciable. You can call StartMetronome()
in your , create an entity and attach the script as a component, and set for example Base and Step to 4, BPM to 120 and launch. In your debug log, you'll have a nice timed "bop" appearing in an endless loop. Mission accomplished.
Wait, we still have something to fix : on usage you’ll realize that this is precise but not enough. It tends to desynchronize pretty fast (at 120bpm, it’s be off tracks in less than 4 measures) and that’s bad if for instance you’re making a musical game. The reason is simple : coroutines are balanced by frames, and frames have a delta that you don’t really control. The problem is that your interval is fixed, but the WaitForSeconds might just decide that it’s too late to execute at this frame, let’s wait another one or two. Thus the wibbly wobbly bullshit the metronome outputs. This is where comes in. The purpose is to resync the metronome with effective time scales. The wait interval will thus never be constant. Let’s modify our methods :
public void StartMetronome()
{
StopCoroutine("DoTick");
CurrentStep = 1;
var multiplier = Base / 4f;
var tmpInterval = 60f / BPM;
interval = tmpInterval / multiplier;
nextTime = Time.time; // set the relative time to now
StartCoroutine("DoTick");
}
IEnumerator DoTick() // yield methods return IEnumerator
{
for (; ; )
{
Debug.Log("bop");
// do something with this beat
nextTime += interval; // add interval to our relative time
yield return new WaitForSeconds(nextTime - Time.time); // wait for the difference delta between now and expected next time of hit
CurrentStep++;
if (CurrentStep > Step)
{
CurrentStep = 1;
CurrentMeasure++;
}
}
}
This very simple trick allows you to fix this sync problem, as the wait delta will be fixed depending on actual time and expected time. Of course, this is far from perfect, but it does the trick : beats are really precise, even after 20 minutes of play. The rest is up to you. I decided to implement events on tick and on new measure, and you can find my code sample on this gist. Output is as follows with a visual thingy:
Anyways, have fun, code safe.
Tuxic
Originally published at http://cubeslam.net on December 19, 2013.