Metronome

So, I decided to try and build a Kismet-based metronome system so that I can trigger small musical elements (reliably through a sort of music sequencer type system) and also in-game events (eg. enemy spawn, explosions, etc…) in time with the music. Originally I tried to do this directly in Kismet using looping Delay objects, but I found that they didn’t keep reliable time.

The first attempt involved using the SetTimer() function, which was set to the 16th beat division of a given tempo (eg. 125ms @ 120bpm). This was then used to ‘fire’ a custom Kismet event. But again, I found that the timing was unreliable.
After some research, I surmised that this was due to the fact that Actors in UDK are updated every tick and so the delay function could be out by a full frame length (approx. 20ms on my system).

So, I decided to do the following:
Establish the ‘target’ time of the next beat division by getting the system time and adding the beat division length (eg. 125ms) to this.
Then use the Tick() function to check whether the elapsed time was equal to the ‘target’ time, using the DeltaTime variable (returned within the Tick function).
Now, the elapsed time is unlikely to actual equal the target time so I built in a ‘checking system’ to see if the elapsed time was within 1/2 of a frame length (either under or over) and if so this would be used as the 16th beat trigger. This is not ideal, but I figured that it would only be around 10ms out either way which should be close enough (for now anyways…)
I was also counting the number of 16th beats so that I could trigger 8th/4th/etc. beats as well.

The problem is that when I set up a basic Kismet system to trigger off a sound using my ‘trigger’s it wasn’t keeping reliable time when compared to a long looping drum beat at the same tempo — it feels like its out by more than 10ms.

___________________________________________________________________________

Here’s the code for the Timer (this currently a placeable actor rather than a Kismet Action as at the time this seemed easier, but it will become Kismet-based eventually):

/*
// actor to act as a metronome
// uses time elapsed to check against target time intervals - performed every tick
// setting the ShouldBeOn property (via Kismet) does the following
// - 1 = activate
// - 2 = deactivate
// tempo calculations performed on activation
//
// CHANGES:
// would using an average of DT help with accuracy (would possibly counter effects of occasional long tick time)????
// need to fire off all Kismet events & update 16thBeat Count on start
//
*/
class AwesomeTimerActor extends Actor
 hidecategories(Attachment, Physics, Debug, Object) // hide these categories in the properties of this actor
 placeable;
var StaticMeshComponent MyMesh;
var int ShouldBeOn;
var float Tempo;
var float DivLength16th;
var int Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec;
var int SystemStartTimeIntA, SystemStartTimeIntB;
var int DeltaTimeMsec, accDT, TargetTime, CurrentTime, Count16thBeats;
var int StartTime;
function Tick(float DeltaTime)
{
 //`log(DeltaTime);
if(ShouldBeOn == 1) //start Actor functionality
 {
 //`log("======================================== ShouldBeOn == 1");
GetSystemTime( Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec ); //Get the system time
 //`log(" Year:" $ Year $ " Month:" $ Month $ " DayOfWeek:" $ DayOfWeek $ " Day:" $ Day $ " Hour:" $ Hour $ " Min:" $ Min $ " Sec:" $ Sec $ " MSec:" $ MSec); //log system time
SystemStartTimeIntA = (Year * 10000) + (Month * 100) + (Day); //combine Year, Month & Day - done to preserve leading 0's
 //`log("SystemStartTimeIntA: " $ SystemStartTimeIntA);
 SystemStartTimeIntB = (Hour * 10000000) + (Min * 100000) + (Sec * 1000) + (MSec); //combine Hour, Min, Sec & MSec - done to preserve leading 0's
 //`log("SystemStartTimeIntB: " $ SystemStartTimeIntB);
DivLength16th = ((1000 / (Tempo / 60)) / 4); //calculate lenth (in msec) of 16th beat division
 //`log("DivLength16th: " $ DivLength16th);
Count16thBeats = 1; //number of 16th beat divisions
ShouldBeOn = 3; //set to 3 so can use another if check - used to move into the metronome system
 }
if(ShouldBeOn == 2) //stop Actor functionality
 {
 //`log("========================================= ShouldBeOn == 2");
 ShouldBeOn = 0;
 }
if(ShouldBeOn == 3) //metronome system
 {
 DeltaTimeMSec = DeltaTime * 1000; //convert DT from secs to msecs
 //`log("DeltatimeMSec: " $ DeltaTimeMSec);
 accDT += DeltaTimeMSec; //how much time has elapsed (in msec) since start
 //`log("accDT: " $ accDT);
 TargetTime = SystemStartTimeIntB + (DivLength16th * Count16thBeats); //calculate target time of next 16th beat division
 //`log("TargetTime: " $ TargetTime);
 CurrentTime = SystemStartTimeIntB + accDT; //increase current time
 //`log("CurrentTime: " $ CurrentTime);
 
 if(CurrentTime == TargetTime) //if current time is equal to target time (unlikely, but possible)
 {
 //`log("16th Beat Trigger - equal");
 TriggerKismet16thBeat(); //Trigger Kismet 16th Beat
if(Count16thBeats % 2 == 0) //check for 8th Beat
 {
 //`log("8th Beat Trigger");
 TriggerKismet8thBeat(); //Trigger Kismet 8th Beat
 }
if(Count16thBeats % 4 == 0) //check for 4th Beat
 {
 //`log("4th Beat Trigger");
 TriggerKismet4thBeat(); //Trigger Kismet 4th Beat
 }
if(Count16thBeats % 16 == 0) //check for 1 Bar
 {
 //`log("1 Bar Trigger");
 TriggerKismet1Bar(); //Trigger Kismet 1 Bar
 }
if(Count16thBeats % 32 == 0) //check for 2 Bar
 {
 //`log("2 Bar Trigger");
 TriggerKismet2Bar(); //Trigger Kismet 2 Bar
 }
Count16thBeats++; //increase count of 16th beat divisions
 }
else if(CurrentTime >= (TargetTime - (DeltaTimeMSec / 2)) && CurrentTime <= TargetTime) //if current time is less than 1/2DT under target time (ie. close enough)
 {
 //`log("16th Beat Trigger - under");
 TriggerKismet16thBeat(); //Trigger Kismet 16th Beat
if(Count16thBeats % 2 == 0) //check for 8th Beat
 {
 //`log("8th Beat Trigger");
 TriggerKismet8thBeat(); //Trigger Kismet 8th Beat
 }
if(Count16thBeats % 4 == 0) //check for 4th Beat
 {
 //`log("4th Beat Trigger");
 TriggerKismet4thBeat(); //Trigger Kismet 4th Beat
 }
if(Count16thBeats % 16 == 0) //check for 1 Bar
 {
 //`log("1 Bar Trigger");
 TriggerKismet1Bar(); //Trigger Kismet 1 Bar
 }
if(Count16thBeats % 32 == 0) //check for 2 Bar
 {
 //`log("2 Bar Trigger");
 TriggerKismet2Bar(); //Trigger Kismet 2 Bar
 }
Count16thBeats++; //increase count of 16th beat divisions
 }
else if(CurrentTime <= (TargetTime + (DeltaTimeMSec / 2)) && CurrentTime >= TargetTime) //if current time is less then 1/2DT over target time (ie. close enough)
 {
 //`log("16th Beat Trigger - over");
 TriggerKismet16thBeat(); //Trigger Kismet 16th Beat
if(Count16thBeats % 2 == 0) //check for 8th Beat
 {
 //`log("8th Beat Trigger");
 TriggerKismet8thBeat(); //Trigger Kismet 8th Beat
 }
if(Count16thBeats % 4 == 0) //check for 4th Beat
 {
 //`log("4th Beat Trigger");
 TriggerKismet4thBeat(); //Trigger Kismet 4th Beat
 }
if(Count16thBeats % 16 == 0) //check for 1 Bar
 {
 //`log("1 Bar Trigger");
 TriggerKismet1Bar(); //Trigger Kismet 1 Bar
 }
if(Count16thBeats % 32 == 0) //check for 2 Bar
 {
 //`log("2 Bar Trigger");
 TriggerKismet2Bar(); //Trigger Kismet 2 Bar
 }
Count16thBeats++; //increase count of 16th beat divisions
 }
 //`log(""); //used to put a line break into log to separate data streams
 }
}

function TriggerKismet2Bar()
{
 //`log("============================= Timer2Bar");
 TriggerEventClass(class'AwesomeSeqEvent_Timer2Bar', self);
}
function TriggerKismet1Bar()
{
 //`log("============================= Timer1Bar");
 TriggerEventClass(class'AwesomeSeqEvent_Timer1Bar', self);
}
function TriggerKismet4thBeat()
{
 //`log("============================== Timer4thBeat");
 TriggerEventClass(class'AwesomeSeqEvent_Timer4thBeat', self);
}
function TriggerKismet8thBeat()
{
 //`log("================================== Timer8thBeat");
 TriggerEventClass(class'AwesomeSeqEvent_Timer8thBeat', self);
}
function TriggerKismet16thBeat()
{
 //`log("================================== Timer16thBeat");
 TriggerEventCLass(class'AwesomeSeqEvent_Timer16thBeat', self);
}
defaultproperties
{
 CustomTimeDilation=1.0
Tempo=120
ShouldBeOn=0
SupportedEvents.Add(class'AwesomeSeqEvent_Timer2Bar')
 SupportedEvents.Add(class'AwesomeSeqEvent_Timer1Bar')
 SupportedEvents.Add(class'AwesomeSeqEvent_Timer4thBeat')
 SupportedEvents.Add(class'AwesomeSeqEvent_Timer8thBeat')
 SupportedEvents.Add(class'AwesomeSeqEvent_Timer16thBEat')
// add a static mesh with a material
 Begin Object Name=PickupMesh
 StaticMesh=StaticMesh'UN_SimpleMeshes.TexPropCube_Dup'
 Materials(0)=Material'EditorMaterials.WidgetMaterial_Y'
 Scale3D=(X=0.125,Y=0.125,Z=0.125)
 End Object
 Components.Add(PickupMesh)
 MyMesh=PickupMesh
}

___________________________________________________________________________

Here’s the code for the Kismet Events:

class AwesomeSeqEvent_Timer16thBeat extends SequenceEvent;
event Activated()
{
 //`log("=============================== Timer8thBeat EventActivated");
}
defaultproperties
{
 MaxTriggerCount=0
 ObjName="Timer16thBeat"
 ObjCategory="Awesome Game"
 VariableLinks.Empty
 bPlayerOnly=false
}

______________

class AwesomeSeqEvent_Timer8thBeat extends SequenceEvent;
event Activated()
{
 //`log("=============================== Timer8thBeat EventActivated");
}
defaultproperties
{
 MaxTriggerCount=0
 ObjName="Timer8thBeat"
 ObjCategory="Awesome Game"
 VariableLinks.Empty
 bPlayerOnly=false
}

_______________

class AwesomeSeqEvent_Timer4thBeat extends SequenceEvent;
event Activated()
{
 //`log("=============================== Timer4thBeat EventActivated");
}
defaultproperties
{
 MaxTriggerCount=0
 ObjName="Timer4thBeat"
 ObjCategory="Awesome Game"
 VariableLinks.Empty
 bPlayerOnly=false
}

_______________

class AwesomeSeqEvent_Timer2Bar extends SequenceEvent;
event Activated()
{
 //`log("=============================== Timer2Bar EventActivated");
}
defaultproperties
{
 MaxTriggerCount=0
 ObjName="Timer2Bar"
 ObjCategory="Awesome Game"
 VariableLinks.Empty
 bPlayerOnly=false
}

_______________

class AwesomeSeqEvent_Timer1Bar extends SequenceEvent;
event Activated()
{
 //`log("=============================== Timer1Bar EventActivated");
}
defaultproperties
{
 MaxTriggerCount=0
 ObjName="Timer1Bar"
 ObjCategory="Awesome Game"
 VariableLinks.Empty
 bPlayerOnly=false
}

___________________________________________________________________________

Now, I’m not sure if the unreliable timing is just the way it is within UDK without proper access to the underlying engine code (I’m not a developer) or if its due to inefficiencies in my code..?