Calculating Song Position in Music Apps with Swift and AVAudioEngine

Fundamentally, music is just sound organized in time. So when building musical tools, it’s helpful to indicate just where you are in time.
Familiar interfaces like the position indicator found in Logic solve this in the transport bar:

And if you happen to be using a MIDI sequencer like AVAudioSequencer you can access that value with handy properties like currentPositionInBeats: TimeInterval and currentPositionInSeconds: TimeInterval.
But how do you do this with a vanilla AVAudioEngine based application? Maybe you have an app that just wants to monitor recording time or augment a real time performance with beat information?
Capturing a Reference Time
You can only determine the current position if you have some some kind of reference time. Capturing this value is probably going to be associated with some other kind of event like starting playback or triggering an instrument for the first time.
It’s convenient to just grab a reference time from the AVAudioEngine’s default output node.
var engine = AVAudioEngine()
var offsetTime = engine.outputNode.lastRenderTimeCalculating Current Position in Seconds
Calculating the current position is really just a matter of taking the current time and subtracting the reference time. This makes a good candidate for a computed instance variable.
Here I’m using the difference in audio frames and dividing by the sample rate to get the duration seconds.
var currentPositionInSeconds: TimeInterval {
get {
let lastRenderTime = engine.outputNode.lastRenderTime
else { return 0 }
let frames = lastRenderTime.sampleTime -
offsetTime.sampleTime
return Double(frames) / offsetTime.sampleRate
}
}Calculating Current Position in Beats
If you have tempo information, current position in beats is probably more helpful. With the beat format you can also track things like measures and divisions.
Utilizing this simple Tempo struct I previously wrote about, we can just divide currentPositionInSeconds by seconds in beat.
let myTempo = Tempo(bpm: 120.0)
var currentPositionInBeats: TimeInterval {
get { return currentPositionInSeconds / tempo.seconds() }
}Displaying Beats
Much like AVAudioSequencer’s currentPositionInBeats this will output values like 1.125, 2.25, and many small positions in between.
To calculate measures or bars, first you need to know how many beats are in a measure. For 4/4 time, that would just be 4. And since you only want whole numbers, you round down.
let position = audioEngine.currentPositionInBeats
let bars = Int(floor(position / 4))To display something like 1.2 to indicate the first measure, second beat, you just need to look at the remainders.
let bars = Int(floor(position / 4))
let beats = Int(floor(position.truncatingRemainder(dividingBy: 4)))
print("\(bars).\(beats)")For more granularity you can just use additional divisions with values like 1/32, display the raw remainder, or display something else like ticks.
