Format realtime stopwatch timer to the hundredth using Swift - swift

I have an app using an NSTimer at centisecond (0.01 second) update intervals to display a running stopwatch in String Format as 00:00.00 (mm:ss.SS). (Basically cloning the iOS built-in stopwatch to integrate into realtime sports timing math problems, possibly needing millisecond accuracy in the future)
I use (misuse?) the NSTimer to force-update the UILabel. If the user presses Start, this is the NSTimer code used to start repeating the function:
displayOnlyTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("display"), userInfo: nil, repeats: true)
And here is the function that is executed by the above NSTimer:
func display() {
let currentTime = CACurrentMediaTime() - timerStarted + elapsedTime
if currentTime < 60 {
timeDisplay.text = String(format: "%.2f", currentTime)
}else if currentTime < 3600 {
var minutes = String(format: "%00d", Int(currentTime/60))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = minutes + ":" + seconds
}else {
var hours = String(format: "%00d", Int(currentTime/3600))
var minutes = String(format: "%02d", (Int(currentTime/60)-(Int(currentTime/3600)*60)))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = hours + ":" + minutes + ":" + seconds
}
}
There will be at least 2 display links running at the same time. Will this method be too inefficient once all other elements are in play?
The display is then updated without using NSTimer when the user presses stop/pause/reset. I didn't find anything that directly translated into Swift. I'm fairly certain I'm using an inefficient method to force update the text UILabel quickly in the UIView.
More Details:
I'm working on less messy code for the running timer format (mm:ss.SS). I will update this once more when I've finished that.
UPDATE: Thanks to Rob and jtbandes for answering both of my questions (formatting method and display update method).
It was easy to replace the NSTimer (see above) with CADisplayLink():
displayLink = CADisplayLink(target: self, selector: Selector("display"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
And then replace all instances in code of
displayOnlyTimer.invalidate()
with
displayLink.paused = true
(this will pause the display link from updating)

For rapid UI updates you should use a CADisplayLink. Anything faster than the display refresh rate is a waste of processing power since it physically cannot be displayed. It also provides a timestamp of the previous frame so you can try to predict when the next frame will be.
You're calculating CACurrentMediaTime() - timerStarted + elapsedTime multiple times. I would recommend doing it only once and saving it in a local variable.
Consider using NSDateComponentsFormatter. Try to reuse one instance of the formatter rather than creating a new one each time (which is usually the most expensive part). Overall, the less string manipulation you can do, the better.
You can check CACurrentMediaTime at the beginning and end of your display method to see how long it takes. Ideally it should be much less than 16.6ms. Keep an eye on the CPU usage (and general power consumption) in the Xcode debug navigator.

I was solving the same problem today and found this answer. The Rob's and jtbandes' advices are helped a lot and i was able to assemble the clean and working solution from around the internet. Thanks you guys. And thanks to mothy for the question.
I've decided to use CADisplayLink because there is no point in firing timer's callback more often than the screen updates:
class Stopwatch: NSObject {
private var displayLink: CADisplayLink!
//...
override init() {
super.init()
self.displayLink = CADisplayLink(target: self, selector: "tick:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
//...
}
//...
}
I'm tracking time by incrementing the elapsedTime variable by displayLink.duration each tick:
var elapsedTime: CFTimeInterval!
override init() {
//...
self.elapsedTime = 0.0
//...
}
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
//...
}
Time-formatting is done through NSDateFormatter:
private let formatter = NSDateFormatter()
override init() {
//...
formatter.dateFormat = "mm:ss,SS"
}
func elapsedTimeAsString() -> String {
return formatter.stringFromDate(NSDate(timeIntervalSinceReferenceDate: elapsedTime))
}
The UI can be updated in the callback closure which Stopwatch calls on every tick:
var callback: (() -> Void)?
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
// Calling the callback function if available
callback?()
}
And that's all you need to do in the ViewController to utilize the Stopwatch:
let stopwatch = Stopwatch()
stopwatch.callback = self.tick
func tick() {
elapsedTimeLabel.text = stopwatch.elapsedTimeAsString()
}
Here is the gist with the full code of Stopwatch and usage guide:
https://gist.github.com/Flar49/06b8c9894458a3ff1b14
I hope that this explanation and gist will help others who will stumble upon this thread in the future with the same problem :)

Related

Create a stopwatch in Swift using .timeIntervalSince()

I need to create a simple stopwatch in Swift using the method timeIntervalSince().
I don't really understand how to use timeIntervalSince (what I need and how to implement it) and how to transform it into a String that will show me the passed time like "00:00:00".
I know I need to use a Timer to update the Label and invalidate it when clicking on "Stop".
I'd really appreciate any help on this. Let me know if you need more information.
The method timeIntervalSince(_:) is a method of Date. It gives you the number of seconds that have passes since some other date and the date you are asking.
So,
Create a StopwatchVC.
Give StopwatchVC a startTime var of type Date.
Also give it a Timer var. Lets call that updateTimer.
When the user taps the start button, save Date() (the time right now) into startTime. Also start a repeating timer, updateTimer that fires every 1/10 second. (or however often you want to update your stopwatch, but note that faster than 1/60 is pointless because the screen can't update any faster than that, and timers are only accurate to about 1/50 sec anyway.)
Each time updateTimer fires, calculate the number of seconds that have elapsed since the start time and display it to the screen:
let seconds = Date().timeIntervalSince(startTime)
Date() is the current date and time, with sub-millisecond accuracy.
Date().timeIntervalSince(startTime) will give you the number of seconds since startTime, again with sub-millisecond accuracy.
Format and display the elapsed time to the screen. You could use a DateComponentsFormatter or build a time string yourself using a NumberFormatter, or even String(format:)
//
// StopWatchVC.swift
// Gem
//
// Created by Macbook 5 on 4/18/22.
//
import UIKit
class StopWatchVC:UIViewController {
var timer:Timer?
var startTime = Date()
let titleLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(titleLabel)
titleLabel.frame = CGRect(x: 0, y: 0, width: 200, height: 60)
titleLabel.center = view.center
titleLabel.textColor = .red
view.backgroundColor = .white
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
let timeInterval = Date().timeIntervalSince(startTime)
titleLabel.text = timeInterval.stringFromTimeInterval()
}
}
extension TimeInterval{
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let ms = Int((self.truncatingRemainder(dividingBy: 1)) * 1000)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
return String(format: "%0.2d:%0.2d:%0.2d.%0.3d",hours,minutes,seconds,ms)
}
}

Swift, iOS: How to detect GCController button being held when only given a pressedChangedHandler?

My solution is hacky and frankly doesn't even work because the spin loop halts other simultaneous button actions from being recognized. I'd love to be able to use a long press gesture recognizer, but that 's not available on GCController.
let dPadLeftButtonPressedChangedHandler = {
(dPadLeftButton: GCControllerButtonInput, value: Float, pressed: Bool) -> Void in
if dPadLeftButton.isPressed {
// No longer nil
self.moveLeft()
self.nanoTimer = DispatchTime.now()
while (dPadLeftButton.isPressed) {
let nanoTimerNow = DispatchTime.now()
// Thousandths of a second
let timeDifference = (nanoTimerNow.uptimeNanoseconds - self.nanoTimer!.uptimeNanoseconds) / UInt64(1000000)
// 200 / 1000 is .2 seconds
if timeDifference > 250 {
self.hardLeft()
self.nanoTimer = nil
break
}
}
}
}
controller?.extendedGamepad?.dpad.left.pressedChangedHandler = dPadLeftButtonPressedChangedHandler
I solved this by initialzing a CADisplayLink object to call the function I want repeated via the selector argument. Then on the button release, I invalidate the CADisplayLink.

Implementing NSTimer in MVVM architecture

I want to implement a NSTimer to show a chronometer using a NSTimeInterval, so I looked around and found this code, which I put into my ViewModel layer:
public class ViewModel {
public func startTimer() {
//if !timer.valid {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
//}
}
#objc public func updateTime() -> String {
let currentTime = NSDate.timeIntervalSinceReferenceDate()
//Find the difference between current time and start time.
var elapsedTime: NSTimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (NSTimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(seconds)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
return "\(strMinutes):\(strSeconds)"
}
}
And I want to show the current elapsed time on my view, so tried this but it didn't work:
viewModel?.startTimer()
timerLabel.text = viewModel?.updateTime()
How can I show the latest result of updateTime() on my ViewController label?
The updateTime method cannot just return the string. It has to initiate the updating of the label in question. You can either code it to update the label directly, or you can provide a closure that updateTime will call when it has the string value.
I tried to implement Rob's answer, but couldn't really get the grasp of CAdisplayLink in a MVVM architecture and came up with the same problem of updating a GUI element periodically in a different view.
However I used my RAC knowledge and created and passed a RACSignal to update my ViewController:
RACSignal.interval(1.0, onScheduler: .mainThreadScheduler()).subscribeNext({ _ in
self.timerLabel.text = self.viewModel?.updateTime()
})

How do I make this a timed loop? Swift [duplicate]

I am trying to build a reliable solid system to build a metronome in my app using SWIFT.
I Have built what seems to be a solid system using NSTimer so far.. The only issue I am having right now is when the timer starts the first 2 clicks are off time but then it catches into a solid timeframe.
Now after all my research I have seen people mention you should use other Audio tools not relying on NSTimer.. Or if you choose use NSTimer then it should be on its own thread. Now I see many confused by this Including myself and I would love to get down to the bottom of this Metronome business and get this solved and share it with all those who are struggling.
UPDATE
So I have implemented and cleaned up at this point after the feedback I had last recieved. At this point here is how my code is structured. Its playing back. But I am still getting 2 fast clicks in the beginning and then it settles in.
I apologize on my ignorance for this one. I hope I am on the right path.
I currently am prototyping another method as well. Where I have a very small audio file with one click and dead space at the end of it with the correct duration until for a loop point for specific tempos. I am looping this back and works very well. But the only thing Is I dont get to detect the loop points for visual updates so I have my basic NStimer just detecting the timing intervals underneath the audio being processed and it seems to matchup very well throughout and no delay. But I still would rather get it all with this NSTimer. If you can easily spot my error would be great for one more kick in the right direction and I am sure it can work soon! Thanks so much.
//VARIABLES
//AUDIO
var clickPlayer:AVAudioPlayer = AVAudioPlayer()
let soundFileClick = NSBundle.mainBundle().pathForResource("metronomeClick", ofType: ".mp3")
//TIMERS
var metroTimer = NSTimer()
var nextTimer = NSTimer()
var previousClick = CFAbsoluteTimeGetCurrent() //When Metro Starts Last Click
//Metro Features
var isOn = false
var bpm = 60.0 //Tempo Used for beeps, calculated into time value
var barNoteValue = 4 //How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
var noteInBar = 0 //What Note You Are On In Bar
//********* FUNCTIONS ***********
func startMetro()
{
MetronomeCount()
barNoteValue = 4 // How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
noteInBar = 0 // What Note You Are On In Bar
isOn = true //
}
//Main Metro Pulse Timer
func MetronomeCount()
{
previousClick = CFAbsoluteTimeGetCurrent()
metroTimer = NSTimer.scheduledTimerWithTimeInterval(60.0 / bpm, target: self, selector: Selector ("MetroClick"), userInfo: nil, repeats: true)
nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)
}
func MetroClick()
{
tick(nextTimer)
}
func tick(timer:NSTimer)
{
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - previousClick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003)
{
previousClick = CFAbsoluteTimeGetCurrent()
//Play the click here
if noteInBar == barNoteValue
{
clickPlayer.play() //Play Sound
noteInBar = 1
}
else//If We Are Still On Same Bar
{
clickPlayer.play() //Play Sound
noteInBar++ //Increase Note Value
}
countLabel.text = String(noteInBar) //Update UI Display To Show Note We Are At
}
}
A metronome built purely with NSTimer will not be very accurate, as Apple explains in their documentation.
Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer.
I would suggest using an NSTimer that fires on the order of 50 times per desired tick (for example, if you would like a 60 ticks per minute, you would have the NSTimeInterval to be about 1/50 of a second.
You should then store a CFAbsoluteTime which stores the "last tick" time, and compare it to the current time. If the absolute value of the difference between the current time and the "last tick" time is less than some tolerance (I would make this about 4 times the number of ticks per interval, for example, if you chose 1/50 of a second per NSTimer fire, you should apply a tolerance of around 4/50 of a second), you can play the "tick."
You may need to calibrate the tolerances to get to your desired accuracy, but this general concept will make your metronome a lot more accurate.
Here is some more information on another SO post. It also includes some code that uses the theory I discussed. I hope this helps!
Update
The way you are calculating your tolerances is incorrect. In your calculations, notice that the tolerance is inversely proportional to the square of the bpm. The problem with this is that the tolerance will eventually be less than the number of times the timer fires per second. Take a look at this graph to see what I mean. This will generate problems at high BPMs. The other potential source of error is your top bounding condition. You really don't need to check an upper limit on your tolerance, because theoretically, the timer should have already fired by then. Therefore, if the elapsed time is greater than the theoretical time, you can fire it regardless. (For example if the elapsed time is 0.1s and and the actual time with the true BPM should be 0.05s, you should go ahead and fire the timer anyways, no matter what your tolerance is).
Here is my timer "tick" function, which seems to work fine. You need to tweak it to fit your needs (with the downbeats, etc.) but it works in concept.
func tick(timer:NSTimer) {
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - lastTick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003) {
lastTick = CFAbsoluteTimeGetCurrent()
# Play the click here
}
}
My timer is initialized like so: nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)
Ok! You can't get things right basing on time, because somehow we need to deal with DA converters and their frequency - samplerate. We need to tell them the exact sample to start play the sound. Add a single view iOS app with two buttons start and stop and insert this code into ViewController.swift. I keep things simple and it's just an Idea of how we can do this. Sorry for forcing try... This one is made with swift 3. Also check out my project on GitHub https://github.com/AlexShubin/MetronomeIdea
Swift 3
import UIKit
import AVFoundation
class Metronome {
var audioPlayerNode:AVAudioPlayerNode
var audioFile:AVAudioFile
var audioEngine:AVAudioEngine
init (fileURL: URL) {
audioFile = try! AVAudioFile(forReading: fileURL)
audioPlayerNode = AVAudioPlayerNode()
audioEngine = AVAudioEngine()
audioEngine.attach(self.audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
try! audioEngine.start()
}
func generateBuffer(forBpm bpm: Int) -> AVAudioPCMBuffer {
audioFile.framePosition = 0
let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm))
let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: periodLength)
try! audioFile.read(into: buffer)
buffer.frameLength = periodLength
return buffer
}
func play(bpm: Int) {
let buffer = generateBuffer(forBpm: bpm)
self.audioPlayerNode.play()
self.audioPlayerNode.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
}
func stop() {
audioPlayerNode.stop()
}
}
class ViewController: UIViewController {
var metronome:Metronome
required init?(coder aDecoder: NSCoder) {
let fileUrl = Bundle.main.url(forResource: "Click", withExtension: "wav")
metronome = Metronome(fileURL: fileUrl!)
super.init(coder: aDecoder)
}
#IBAction func StartPlayback(_ sender: Any) {
metronome.play(bpm: 120)
}
#IBAction func StopPlayback(_ sender: Any) {
metronome.stop()
}
}
Thanks to the great work already done on this question by vigneshv & CakeGamesStudios, I was able to put together the following, which is an expanded version of the metronome timer discussed here.
Some highlights:
It's updated for Swift v5
It uses a Grand Central Dispatch timer to run on a separate queue, rather than just a regular NSTimer (see here for more details)
It uses more calculated properties for clarity
It uses delegation, to allow for any arbitrary 'tick' action to be handled by the delegate class (be that playing a sound from AVFoundation, updating the display, or whatever else - just remember to set the delegate property after creating the timer). This delegate would also be the one to distinguish beat 1 vs. others, but that'd be easy enough to add within this class itself if desired.
It has a % to Next Tick property, which could be used to update a UI progress bar, etc.
Any feedback on how this can be improved further is welcome!
protocol BPMTimerDelegate: class {
func bpmTimerTicked()
}
class BPMTimer {
// MARK: - Properties
weak var delegate: BPMTimerDelegate? // The class's delegate, to handle the results of ticks
var bpm: Double { // The speed of the metronome ticks in BPM (Beats Per Minute)
didSet {
changeBPM() // Respond to any changes in BPM, so that the timer intervals change accordingly
}
}
var tickDuration: Double { // The amount of time that will elapse between ticks
return 60/bpm
}
var timeToNextTick: Double { // The amount of time until the next tick takes place
if paused {
return tickDuration
} else {
return abs(elapsedTime - tickDuration)
}
}
var percentageToNextTick: Double { // Percentage progress from the previous tick to the next
if paused {
return 0
} else {
return min(100, (timeToNextTick / tickDuration) * 100) // Return a percentage, and never more than 100%
}
}
// MARK: - Private Properties
private var timer: DispatchSourceTimer!
private lazy var timerQueue = DispatchQueue.global(qos: .utility) // The Grand Central Dispatch queue to be used for running the timer. Leverages a global queue with the Quality of Service 'Utility', which is for long-running tasks, typically with user-visible progress. See here for more info: https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
private var paused: Bool
private var lastTickTimestamp: CFAbsoluteTime
private var tickCheckInterval: Double {
return tickDuration / 50 // Run checks many times within each tick duration, to ensure accuracy
}
private var timerTolerance: DispatchTimeInterval {
return DispatchTimeInterval.milliseconds(Int(tickCheckInterval / 10 * 1000)) // For a repeating timer, Apple recommends a tolerance of at least 10% of the interval. It must be multiplied by 1,000, so it can be expressed in milliseconds, as required by DispatchTimeInterval.
}
private var elapsedTime: Double {
return CFAbsoluteTimeGetCurrent() - lastTickTimestamp // Determine how long has passed since the last tick
}
// MARK: - Initialization
init(bpm: Double) {
self.bpm = bpm
self.paused = true
self.lastTickTimestamp = CFAbsoluteTimeGetCurrent()
self.timer = createNewTimer()
}
// MARK: - Methods
func start() {
if paused {
paused = false
lastTickTimestamp = CFAbsoluteTimeGetCurrent()
timer.resume() // A crash will occur if calling resume on an already resumed timer. The paused property is used to guard against this. See here for more info: https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9
} else {
// Already running, so do nothing
}
}
func stop() {
if !paused {
paused = true
timer.suspend()
} else {
// Already paused, so do nothing
}
}
// MARK: - Private Methods
// Implements timer functionality using the DispatchSourceTimer in Grand Central Dispatch. See here for more info: http://danielemargutti.com/2018/02/22/the-secret-world-of-nstimer/
private func createNewTimer() -> DispatchSourceTimer {
let timer = DispatchSource.makeTimerSource(queue: timerQueue) // Create the timer on the correct queue
let deadline: DispatchTime = DispatchTime.now() + tickCheckInterval // Establish the next time to trigger
timer.schedule(deadline: deadline, repeating: tickCheckInterval, leeway: timerTolerance) // Set it on a repeating schedule, with the established tolerance
timer.setEventHandler { [weak self] in // Set the code to be executed when the timer fires, using a weak reference to 'self' to avoid retain cycles (memory leaks). See here for more info: https://learnappmaking.com/escaping-closures-swift/
self?.tickCheck()
}
timer.activate() // Dispatch Sources are returned initially in the inactive state, to begin processing, use the activate() method
// Determine whether to pause the timer
if paused {
timer.suspend()
}
return timer
}
private func cancelTimer() {
timer.setEventHandler(handler: nil)
timer.cancel()
if paused {
timer.resume() // If the timer is suspended, calling cancel without resuming triggers a crash. See here for more info: https://forums.developer.apple.com/thread/15902
}
}
private func replaceTimer() {
cancelTimer()
timer = createNewTimer()
}
private func changeBPM() {
replaceTimer() // Create a new timer, which will be configured for the new BPM
}
#objc private func tickCheck() {
if (elapsedTime > tickDuration) || (timeToNextTick < 0.003) { // If past or extremely close to correct duration, tick
tick()
}
}
private func tick() {
lastTickTimestamp = CFAbsoluteTimeGetCurrent()
DispatchQueue.main.sync { // Calls the delegate from the application's main thread, because it keeps the separate threading within this class, and otherwise, it can cause errors (e.g. 'Main Thread Checker: UI API called on a background thread', if the delegate tries to update the UI). See here for more info: https://stackoverflow.com/questions/45081731/uiapplication-delegate-must-be-called-from-main-thread-only
delegate?.bpmTimerTicked() // Have the delegate respond accordingly
}
}
// MARK: - Deinitialization
deinit {
cancelTimer() // Ensure that the timer's cancelled if this object is deallocated
}
}

Swift Solid Metronome System

I am trying to build a reliable solid system to build a metronome in my app using SWIFT.
I Have built what seems to be a solid system using NSTimer so far.. The only issue I am having right now is when the timer starts the first 2 clicks are off time but then it catches into a solid timeframe.
Now after all my research I have seen people mention you should use other Audio tools not relying on NSTimer.. Or if you choose use NSTimer then it should be on its own thread. Now I see many confused by this Including myself and I would love to get down to the bottom of this Metronome business and get this solved and share it with all those who are struggling.
UPDATE
So I have implemented and cleaned up at this point after the feedback I had last recieved. At this point here is how my code is structured. Its playing back. But I am still getting 2 fast clicks in the beginning and then it settles in.
I apologize on my ignorance for this one. I hope I am on the right path.
I currently am prototyping another method as well. Where I have a very small audio file with one click and dead space at the end of it with the correct duration until for a loop point for specific tempos. I am looping this back and works very well. But the only thing Is I dont get to detect the loop points for visual updates so I have my basic NStimer just detecting the timing intervals underneath the audio being processed and it seems to matchup very well throughout and no delay. But I still would rather get it all with this NSTimer. If you can easily spot my error would be great for one more kick in the right direction and I am sure it can work soon! Thanks so much.
//VARIABLES
//AUDIO
var clickPlayer:AVAudioPlayer = AVAudioPlayer()
let soundFileClick = NSBundle.mainBundle().pathForResource("metronomeClick", ofType: ".mp3")
//TIMERS
var metroTimer = NSTimer()
var nextTimer = NSTimer()
var previousClick = CFAbsoluteTimeGetCurrent() //When Metro Starts Last Click
//Metro Features
var isOn = false
var bpm = 60.0 //Tempo Used for beeps, calculated into time value
var barNoteValue = 4 //How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
var noteInBar = 0 //What Note You Are On In Bar
//********* FUNCTIONS ***********
func startMetro()
{
MetronomeCount()
barNoteValue = 4 // How Many Notes Per Bar (Set To Amount Of Hits Per Pattern)
noteInBar = 0 // What Note You Are On In Bar
isOn = true //
}
//Main Metro Pulse Timer
func MetronomeCount()
{
previousClick = CFAbsoluteTimeGetCurrent()
metroTimer = NSTimer.scheduledTimerWithTimeInterval(60.0 / bpm, target: self, selector: Selector ("MetroClick"), userInfo: nil, repeats: true)
nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)
}
func MetroClick()
{
tick(nextTimer)
}
func tick(timer:NSTimer)
{
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - previousClick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003)
{
previousClick = CFAbsoluteTimeGetCurrent()
//Play the click here
if noteInBar == barNoteValue
{
clickPlayer.play() //Play Sound
noteInBar = 1
}
else//If We Are Still On Same Bar
{
clickPlayer.play() //Play Sound
noteInBar++ //Increase Note Value
}
countLabel.text = String(noteInBar) //Update UI Display To Show Note We Are At
}
}
A metronome built purely with NSTimer will not be very accurate, as Apple explains in their documentation.
Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer.
I would suggest using an NSTimer that fires on the order of 50 times per desired tick (for example, if you would like a 60 ticks per minute, you would have the NSTimeInterval to be about 1/50 of a second.
You should then store a CFAbsoluteTime which stores the "last tick" time, and compare it to the current time. If the absolute value of the difference between the current time and the "last tick" time is less than some tolerance (I would make this about 4 times the number of ticks per interval, for example, if you chose 1/50 of a second per NSTimer fire, you should apply a tolerance of around 4/50 of a second), you can play the "tick."
You may need to calibrate the tolerances to get to your desired accuracy, but this general concept will make your metronome a lot more accurate.
Here is some more information on another SO post. It also includes some code that uses the theory I discussed. I hope this helps!
Update
The way you are calculating your tolerances is incorrect. In your calculations, notice that the tolerance is inversely proportional to the square of the bpm. The problem with this is that the tolerance will eventually be less than the number of times the timer fires per second. Take a look at this graph to see what I mean. This will generate problems at high BPMs. The other potential source of error is your top bounding condition. You really don't need to check an upper limit on your tolerance, because theoretically, the timer should have already fired by then. Therefore, if the elapsed time is greater than the theoretical time, you can fire it regardless. (For example if the elapsed time is 0.1s and and the actual time with the true BPM should be 0.05s, you should go ahead and fire the timer anyways, no matter what your tolerance is).
Here is my timer "tick" function, which seems to work fine. You need to tweak it to fit your needs (with the downbeats, etc.) but it works in concept.
func tick(timer:NSTimer) {
let elapsedTime:CFAbsoluteTime = CFAbsoluteTimeGetCurrent() - lastTick
let targetTime:Double = 60/timer.userInfo!.objectForKey("bpm")!.doubleValue!
if (elapsedTime > targetTime) || (abs(elapsedTime - targetTime) < 0.003) {
lastTick = CFAbsoluteTimeGetCurrent()
# Play the click here
}
}
My timer is initialized like so: nextTimer = NSTimer(timeInterval: (60.0/Double(bpm)) * 0.01, target: self, selector: "tick:", userInfo: ["bpm":bpm], repeats: true)
Ok! You can't get things right basing on time, because somehow we need to deal with DA converters and their frequency - samplerate. We need to tell them the exact sample to start play the sound. Add a single view iOS app with two buttons start and stop and insert this code into ViewController.swift. I keep things simple and it's just an Idea of how we can do this. Sorry for forcing try... This one is made with swift 3. Also check out my project on GitHub https://github.com/AlexShubin/MetronomeIdea
Swift 3
import UIKit
import AVFoundation
class Metronome {
var audioPlayerNode:AVAudioPlayerNode
var audioFile:AVAudioFile
var audioEngine:AVAudioEngine
init (fileURL: URL) {
audioFile = try! AVAudioFile(forReading: fileURL)
audioPlayerNode = AVAudioPlayerNode()
audioEngine = AVAudioEngine()
audioEngine.attach(self.audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
try! audioEngine.start()
}
func generateBuffer(forBpm bpm: Int) -> AVAudioPCMBuffer {
audioFile.framePosition = 0
let periodLength = AVAudioFrameCount(audioFile.processingFormat.sampleRate * 60 / Double(bpm))
let buffer = AVAudioPCMBuffer(pcmFormat: audioFile.processingFormat, frameCapacity: periodLength)
try! audioFile.read(into: buffer)
buffer.frameLength = periodLength
return buffer
}
func play(bpm: Int) {
let buffer = generateBuffer(forBpm: bpm)
self.audioPlayerNode.play()
self.audioPlayerNode.scheduleBuffer(buffer, at: nil, options: .loops, completionHandler: nil)
}
func stop() {
audioPlayerNode.stop()
}
}
class ViewController: UIViewController {
var metronome:Metronome
required init?(coder aDecoder: NSCoder) {
let fileUrl = Bundle.main.url(forResource: "Click", withExtension: "wav")
metronome = Metronome(fileURL: fileUrl!)
super.init(coder: aDecoder)
}
#IBAction func StartPlayback(_ sender: Any) {
metronome.play(bpm: 120)
}
#IBAction func StopPlayback(_ sender: Any) {
metronome.stop()
}
}
Thanks to the great work already done on this question by vigneshv & CakeGamesStudios, I was able to put together the following, which is an expanded version of the metronome timer discussed here.
Some highlights:
It's updated for Swift v5
It uses a Grand Central Dispatch timer to run on a separate queue, rather than just a regular NSTimer (see here for more details)
It uses more calculated properties for clarity
It uses delegation, to allow for any arbitrary 'tick' action to be handled by the delegate class (be that playing a sound from AVFoundation, updating the display, or whatever else - just remember to set the delegate property after creating the timer). This delegate would also be the one to distinguish beat 1 vs. others, but that'd be easy enough to add within this class itself if desired.
It has a % to Next Tick property, which could be used to update a UI progress bar, etc.
Any feedback on how this can be improved further is welcome!
protocol BPMTimerDelegate: class {
func bpmTimerTicked()
}
class BPMTimer {
// MARK: - Properties
weak var delegate: BPMTimerDelegate? // The class's delegate, to handle the results of ticks
var bpm: Double { // The speed of the metronome ticks in BPM (Beats Per Minute)
didSet {
changeBPM() // Respond to any changes in BPM, so that the timer intervals change accordingly
}
}
var tickDuration: Double { // The amount of time that will elapse between ticks
return 60/bpm
}
var timeToNextTick: Double { // The amount of time until the next tick takes place
if paused {
return tickDuration
} else {
return abs(elapsedTime - tickDuration)
}
}
var percentageToNextTick: Double { // Percentage progress from the previous tick to the next
if paused {
return 0
} else {
return min(100, (timeToNextTick / tickDuration) * 100) // Return a percentage, and never more than 100%
}
}
// MARK: - Private Properties
private var timer: DispatchSourceTimer!
private lazy var timerQueue = DispatchQueue.global(qos: .utility) // The Grand Central Dispatch queue to be used for running the timer. Leverages a global queue with the Quality of Service 'Utility', which is for long-running tasks, typically with user-visible progress. See here for more info: https://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2
private var paused: Bool
private var lastTickTimestamp: CFAbsoluteTime
private var tickCheckInterval: Double {
return tickDuration / 50 // Run checks many times within each tick duration, to ensure accuracy
}
private var timerTolerance: DispatchTimeInterval {
return DispatchTimeInterval.milliseconds(Int(tickCheckInterval / 10 * 1000)) // For a repeating timer, Apple recommends a tolerance of at least 10% of the interval. It must be multiplied by 1,000, so it can be expressed in milliseconds, as required by DispatchTimeInterval.
}
private var elapsedTime: Double {
return CFAbsoluteTimeGetCurrent() - lastTickTimestamp // Determine how long has passed since the last tick
}
// MARK: - Initialization
init(bpm: Double) {
self.bpm = bpm
self.paused = true
self.lastTickTimestamp = CFAbsoluteTimeGetCurrent()
self.timer = createNewTimer()
}
// MARK: - Methods
func start() {
if paused {
paused = false
lastTickTimestamp = CFAbsoluteTimeGetCurrent()
timer.resume() // A crash will occur if calling resume on an already resumed timer. The paused property is used to guard against this. See here for more info: https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9
} else {
// Already running, so do nothing
}
}
func stop() {
if !paused {
paused = true
timer.suspend()
} else {
// Already paused, so do nothing
}
}
// MARK: - Private Methods
// Implements timer functionality using the DispatchSourceTimer in Grand Central Dispatch. See here for more info: http://danielemargutti.com/2018/02/22/the-secret-world-of-nstimer/
private func createNewTimer() -> DispatchSourceTimer {
let timer = DispatchSource.makeTimerSource(queue: timerQueue) // Create the timer on the correct queue
let deadline: DispatchTime = DispatchTime.now() + tickCheckInterval // Establish the next time to trigger
timer.schedule(deadline: deadline, repeating: tickCheckInterval, leeway: timerTolerance) // Set it on a repeating schedule, with the established tolerance
timer.setEventHandler { [weak self] in // Set the code to be executed when the timer fires, using a weak reference to 'self' to avoid retain cycles (memory leaks). See here for more info: https://learnappmaking.com/escaping-closures-swift/
self?.tickCheck()
}
timer.activate() // Dispatch Sources are returned initially in the inactive state, to begin processing, use the activate() method
// Determine whether to pause the timer
if paused {
timer.suspend()
}
return timer
}
private func cancelTimer() {
timer.setEventHandler(handler: nil)
timer.cancel()
if paused {
timer.resume() // If the timer is suspended, calling cancel without resuming triggers a crash. See here for more info: https://forums.developer.apple.com/thread/15902
}
}
private func replaceTimer() {
cancelTimer()
timer = createNewTimer()
}
private func changeBPM() {
replaceTimer() // Create a new timer, which will be configured for the new BPM
}
#objc private func tickCheck() {
if (elapsedTime > tickDuration) || (timeToNextTick < 0.003) { // If past or extremely close to correct duration, tick
tick()
}
}
private func tick() {
lastTickTimestamp = CFAbsoluteTimeGetCurrent()
DispatchQueue.main.sync { // Calls the delegate from the application's main thread, because it keeps the separate threading within this class, and otherwise, it can cause errors (e.g. 'Main Thread Checker: UI API called on a background thread', if the delegate tries to update the UI). See here for more info: https://stackoverflow.com/questions/45081731/uiapplication-delegate-must-be-called-from-main-thread-only
delegate?.bpmTimerTicked() // Have the delegate respond accordingly
}
}
// MARK: - Deinitialization
deinit {
cancelTimer() // Ensure that the timer's cancelled if this object is deallocated
}
}