- Home /
Musical beat not in perfect time with tempo. WaitForSeconds not accurate.
My app plays musical chords progressions. Each chord is played for a certain number of beats and behind the chords is a simple metronome beat.
Each of my play events, i.e. a beat audiosource play or a chord audiosource play, is played in order and "yield return new WaitForSeconds()" is used to wait until the next event. I do it like this:
public IEnumerator play()
{
beat1.Stop();
otherBeats.Stop();
float timeBetweenBeats = (60f / (float)bpm);
List<playEvent> eventList = new List<playEvent>();
float totalChordTime = 0f;
for (int i =0; i <allSlots.Count;i++)
{
float timeForThisChord = numberOfBeats(allSlots[i].getNoteLength()) * timeBetweenBeats;
playEvent chordEvent = new playEvent();
chordEvent.beatNo = -1;
chordEvent.chordIndex = i;
chordEvent.chordOrBeat = true;
chordEvent.timeToPlay = Time.time + totalChordTime;
totalChordTime += timeForThisChord;
eventList.Add(chordEvent);
}
float beatTime = 0;
int b = 0;
while (beatTime<totalChordTime)
{
playEvent beat = new playEvent();
beat.beatNo = b;
b++;
beat.chordIndex = -1;
beat.chordOrBeat = false;
beat.timeToPlay = Time.time + beatTime;
beatTime +=timeBetweenBeats;
eventList.Add(beat);
}
eventList.Sort();
float timeToStop = Time.time + totalChordTime;
for (int i = 0; i< eventList.Count;i++)
{
if (Time.time < eventList[i].timeToPlay)
{
yield return new WaitForSeconds(eventList[i].timeToPlay-Time.time);
}
if (!eventList[i].chordOrBeat)
{
if (eventList[i].beatNo%beatsInBar ==0)
{
beat1.Play();
}
else
{
otherBeats.Play();
}
}
else
{
stopAllChords();
allSlots[eventList[i].chordIndex].playChord();
audioSliders[eventList[i].chordIndex].GetComponent<ChordSlider>().moveSlider(Time.time, Time.time + numberOfBeats(allSlots[eventList[i].chordIndex].getNoteLength()) * timeBetweenBeats);
}
if (i== eventList.Count-1) {
yield return new WaitForSeconds(numberOfBeats(allSlots[allSlots.Count - 1].getNoteLength()) * timeBetweenBeats);
}
}
stopAllChords();
resetPlaySliders();
}
However, after debugging and printing lots of parameters, I have found that it is the "WaitForSeconds" function that does not wait for the exact amount of time required. I have a screenshot of some of my logs to the terminal:
For example the logs for the first beat show what time the function is aiming for (14.46284) which occurs 0.6s from the point the log was made, the time before the WaitForSeconds occurs and the time after it. The difference between the two times is 0.614 seconds, which may not sounds like a lot but is definitely noticeable. The music sounds a little bit all over the place and out of time.
What can do i do so that the WaitForSeconds is accurate at least to 2 or 3 decimal places?
Thanks
In a word, nothing.
Not that this is much more informative, but the is no notion of such ti$$anonymous$$g precision compatible with music generation from several of the program$$anonymous$$g concepts being deployed in your example, including the use of coroutines. Frankly, even if you 'fix' it here, C# is going to mess with you at garbage collection time(s).
So I can't get round this in another way, as you say it is not possible with coroutines?
I would say it's more accurate to say coroutines complicate an already complicated issue with ti$$anonymous$$g systems, and with C# generally. C# enters into garbage collection mode periodically, based on what RA$$anonymous$$ has been released. It can create a hiccup because GC freezes the entire application while memory is reorganized. There are C# techniques which limit the amount of garbage an application generates to reduce the impact (sometimes to near zero consequence), but otherwise C# is a layer outside all else that messes with ti$$anonymous$$g. Beyond that, if your code intersects with Unity's main thread, you're code is subordinate to the ti$$anonymous$$g cycle of the Unity engine (the fixedupdate/update cycle).
You may have to resort to a C# thread, and use as precise a timer as C# offers. Coroutines are a Unity appendage for C#, so they'll be 'out' for the most part. You'll be writing a purely C# solution to generating music, possibly sending information to Unity for the actual audio products being initiated (I'm not carefully considering a design, just posting an off the top of my head commentary).
If I had this problem I'd seriously consider a C++ module, but then I'm primarily a C++ programmer and I'm familiar with all of the pitfalls of using a native module (though I'd have to carefully implement in Unity the particulars for each platform).
Answer by VadimMinsk · Aug 22, 2018 at 10:55 AM
just as @Madks13 wrote, WaitForSeconds is not designed to be accurate, it effectively waits for Time+Random.Range(0, FrameTime).
What you can try is to use WaitForFixedUpdate. FixedUpdate ticks independently of the framerate (except for some weird cases where fps drops too low, that is lesser than 4 fps, than FixedUpdates may be discarded).
If you don't use physics, you can even set Fixed update rate equal to your metronome rate.
Great idea! I'm not using physics so that sounds exactly like what i want.
Answer by madks13 · Aug 22, 2018 at 05:46 AM
Umm, this is what happens when people don't understand the tools they are using. I'm not going to go into too technical explanations for the why, but basically what happens is :
The WaitForSecond DOES NOT wait for precisely a second.
The method sends a signal to the system, which takes another one or multiple tasks to do while the waiting method is queued up.
Once a MINIMUM of a second is up, then it goes back to the queued method.
This means that it will alsmost never be exactly a second.
What you are doing is the wrong approach many beginners try to use.
What you SHOULD be doing is find a way to use event based checking to stop/play the beats.
Using event based methods will allow you to react to whichever event you need to coordinate your beats. For example :
Playing a beat after another beat? Set an event on beat ending playing
Event based methods react to other events, so you can chain them infinitely which, for a musical game/application is a much better solution than unity coroutines which add a lot of overhead.
Edit : I wasn't sure if i should add this but here it is :
A coroutine does work in between frames
Yields returns control to Unity so it goes on the next frame
You are basically waiting waiting for n time AND the next frame
This makes your code heavily dependend on the frame rate
Thanks for the advice although I don't appreciate the sarcy nature of the comment. I clearly wouldn't be posting on here if I understood it.