Is there a way to read/get fps in spritekit swift - swift

Is there a method to get the value of fps in swift?
No I dont mean changing the frameInterval
and no I dont mean
skView.showsFPS = true
I want to get the fps so that I can have my app wait until fps reaches 60 before it starts the game. For some reason my app occasionally gets 'stuck' at 40fps lingers for 5 seconds then slowly climbs to back to 60. This usually happens when I summon the ControlCenter.

You can calculate it yourself.
Doing something like this in your scene:
var lastUpdateTime: TimeInterval = 0
func update(currentTime: TimeInterval) {
let deltaTime = currentTime - lastUpdateTime
let currentFPS = 1 / deltaTime
print(currentFPS)
lastUpdateTime = currentTime
}

There's another way to get the FPS of the view itself independent from which scene is presented, using SKViewDelegate (taken from the Developer Documentation, just with the added FPS property):
class ViewController: NSViewController, SKViewDelegate {
let spriteView: SKView = SKView()
override func viewDidLoad() {
self.spriteView.delegate = self // Set the view's delegate to the ViewController.
}
let targetFps: TimeInterval = 60 // Set to your preferred fps
var lastRenderTime: TimeInterval = 0
var fps: TimeInterval = 0.0 // Access this property to get the view's fps
// Delegate method:
public func view(_ view: SKView, shouldRenderAtTime time: TimeInterval) -> Bool {
if time - lastRenderTime >= 1 / targetFps { // Check if the scene should render.
// Determine the fps with the time since the last render:
self.fps = 1 / (time - lastRenderTime)
// Set the last render time to the current time:
self.lastRenderTime = time
// Let scene know that it should render the next frame.
return true
}
else {
return false
}
}
}

gameView.showsStatistics = true

Related

Keep particles in scene paused after app becomes active

Let me preface this question by saying I know it has been asked a few times, but the most upvoted post here was from back in 2014 and the top answer doesn't seem to work anymore.
Issue
In my game, I have all my 'moving' nodes as children of pauseNode. When the pauses button is pressed or when the player leaves the app, pauseScene() is called:
func pauseScene() {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
}
This pauses all sprite nodes and emitter nodes. This keeps the sprite nodes paused if the app transitions from background to foreground state, but the particle emitters seem to resume animating.
Current solution
I solved this issue by triggering pauseScene() after a small delay when the app became active.
override func didMove(to view: SKView) {
NotificationCenter.default.addObserver(self, selector: #selector(GameScene.applicationDidBecomeActive(notification:)), name: NSNotification.Name.UIApplicationDidBecomeActive, object: app)
}
#objc func applicationDidBecomeActive(notification: NSNotification) {
NSDelay(0.01) {
pauseScene()
}
}
func NSDelay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
Goal
While this solution pauses the particles, it is not perfect because they are visible moving for the split second it takes to pause them again (this is the shortest delay that actually re-pauses them).
So, after reading the post I linked above, I tried to override applicationDidBecomeActive() in an SKView subclass as was suggested, but I couldn't get the method to be called. I want to prevent these particles from being unpaused in the first place, if possible. Thanks
So, it's been a little over a day and I've found a better solution. A comment in this post suggested having a variable that recorded the status of the scene, and updating it along with the pause/play functions:
var sceneIsPaused = false
func pauseScene() {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
sceneIsPaused = true
}
func playScene() {
self.node.speed = 1
self.physicsWorld.speed = 1
self.node.isPaused = false
sceneIsPaused = false
}
Then, override SKView's update method and check the state of that variable. If the scene should be paused, pause it. This will mean that if the scene is automatically unpaused it will re-pause the next frame. This is much faster and cleaner than setting a delay:
override func update(_ currentTime: TimeInterval) {
if (sceneIsPaused == true) {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
}
}

Get frame's position at an specific rate per second

I'm trying to get the current position of camera in scene adding a renderer delegate method (a method in ARSCNViewDelegate):
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let position = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let orientation = SCNVector3Make(transform.m31, transform.m32, transform.m33)
...
}
And I would like to get these positions for a specific number of frames per second (25 fps in my case). I can get this by modifying this parameter: preferredFramesPerSecond but it always adjusts automatically to 30 fps, not 25.
It works ok for 5, 15, 20, 30, 45 or 60, but why not for 25 fps?
override func viewDidLoad() {
super.viewDidLoad()
...
sceneView.preferredFramesPerSecond = 25
...
}
Anyway, of course reducing FPS means poorer AR performance so... is there another way to ask ARKit for a frame camera position each time I need one? specifically every 25 fps.
Thanks a lot in advance!
This is used to run block of code after certain time in second. (1/25 in your case)
func recurringFunction(){
// your code here
guard let pointOfView = sceneView.pointOfView else { return }
let transform = pointOfView.transform
let position = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let orientation = SCNVector3Make(transform.m31, transform.m32, transform.m33)
....
DispatchQueue.main.asyncAfter(deadline: .now() + 1/25) {
self.recurringFunction()
}
}
It will also help in running your app without dropping your usual fps.

Smooth animation with timer and loop in iOS app

I have ViewController with stars rating that looks like this (except that there are 10 stars)
When user opens ViewController for some object that have no rating I want to point user's attention to this stars with very simple way: animate stars highlighting (you could see such behaviour on some ads in real world when each letter is highlighted one after another).
One star highlighted
Two stars highlighted
Three stars highlighted
......
Turn off all of them
So this is the way how I am doing it
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
func ratingStarsAnimation() {
for i in 1...11 {
var timer : Double = 0.6 + Double(i)*0.12
delayWithSeconds(timer) {
ratingStars.rating = (i < 10) ? Double(i) : 0
}
}
}
What is going on here? I have function called delayWithSeconds that delays action and I use this function to delay each star highlighting. And 0.6 is initial delay before animation begins. After all stars are highlighted - last step is to turn off highlighting of all stars.
This code works but I can't say that it is smooth.
My questions are:
How can I change 0.6 + Double(i)*0.12 to get smooth animation feel?
I think that my solution with delays is not good - how can I solve smooth stars highlighting task better?
Have a look at the CADisplaylink class. Its a specialized timer that is linked to the refresh rate of the screen, on iOS this is 60fps.
It's the backbone of many 3rd party animation libraries.
Usage example:
var displayLink: CADisplayLink?
let start: Double = 0
let end: Double = 10
let duration: CFTimeInterval = 5 // seconds
var startTime: CFTimeInterval = 0
let ratingStars = RatingView()
func create() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .defaultRunLoopMode)
}
func tick() {
guard let link = displayLink else {
cleanup()
return
}
if startTime == 0 { // first tick
startTime = link.timestamp
return
}
let maxTime = startTime + duration
let currentTime = link.timestamp
guard currentTime < maxTime else {
finish()
return
}
// Add math here to ease the animation
let progress = (currentTime - startTime) / duration
let progressInterval = (end - start) * Double(progress)
// get value =~ 0...10
let normalizedProgress = start + progressInterval
ratingStars.rating = normalizedProgress
}
func finish() {
ratingStars.rating = 0
cleanup()
}
func cleanup() {
displayLink?.remove(from: .main, forMode: .defaultRunLoopMode)
displayLink = nil
startTime = 0
}
As a start this will allow your animation to be smoother. You will still need to add some trigonometry if you want to add easing but that shouldn't be too difficult.
CADisplaylink:
https://developer.apple.com/reference/quartzcore/cadisplaylink
Easing curves: http://gizma.com/easing/

Correct handling / cleanup / etc of CADisplayLink in Swift custom animation?

Consider this trivial sync animation using CADisplayLink,
var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4
private func yourAnim()
{
if ( link != nil )
{
link!.paused = true
//A:
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
}
link = CADisplayLink(target: self, selector: #selector(doorStep) )
startTime = CACurrentMediaTime()
link!.addToRunLoop(
NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
}
func doorStep()
{
let elapsed = CACurrentMediaTime() - startTime
var ping = elapsed
if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}
let frac = ping / (animTime / 2.0)
yourAnimFunction(CGFloat(frac) * animMaxVal)
if (elapsed > animTime)
{
//B:
link!.paused = true
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
yourAnimFunction(0.0)
}
}
func killAnimation()
{
// for example if the cell disappears or is reused
//C:
????!!!!
}
There seems to be various problems.
At (A:), even though link is not null, it may not be possible to remove it from a run loop. (For example, someone may have initialized it with link = link:CADisplayLink() - try it for a crash.)
Secondly at (B:) it seems to be a mess ... surely there's a better (and more Swift) way, and what if it's nil even though the time just expired?
Finally in (C:) if you want to break the anim ... I got depressed and have no clue what is best.
And really the code at A: and B: should be the same call right, kind of a clean-up call.
Here’s a simple example showing how I’d go about implementing a CADisplayLink (in Swift 5):
class C { /// your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0
func startDisplayLink() {
stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
#objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}
/// do your animation logic here
}
/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}
Points to note:
We’re using nil here to represent the state in which the display link isn’t running – as there’s no easy way of getting this information from an invalidated display link.
Instead of using removeFromRunLoop(), we’re using invalidate(), which will not crash if the display link hasn’t already been added to a run-loop. However this situation should never arise in the first place – as we’re always immediately adding the display link to the run-loop after creating it.
We’ve made the displayLink private in order to prevent outside classes from putting it in an unexpected state (e.g invalidating it but not setting it to nil).
We have a single stopDisplayLink() method that both invalidates the display link (if it is non-nil) and sets it to nil – rather than copy and pasting this logic.
We’re not setting paused to true before invalidating the display link, as this is redundant.
Instead of force unwrapping the displayLink after checking for non-nil, we’re using optional chaining e.g displayLink?.invalidate() (which will call invalidate() if the display link isn’t nil). While force unwrapping may be ‘safe’ in your given situation (as you’re checking for nil) – it’s potentially unsafe when it comes to future refactoring, as you may re-structure your logic without considering what impact this has on the force unwraps.
We’re clamping the elapsed time to the animation duration in order to ensure that the later animation logic doesn’t produce a value out of the expected range.
Our update method displayLinkDidFire(_:) takes a single argument of type CADisplayLink, as required by the documentation.
I realize this question already has a good answer, but here's another slightly different approach that helps in implementing smooth animations independent of the display link frame rate.
**(Link to demo project available at the bottom of this answer - UPDATE: demo project source code now updated to Swift 4)
For my implementation I opted to wrap the display link in it's own class and setup a delegate reference that will get called with the delta time (the time between the last display link call and the current call) so we can perform our animations a little more smoothly.
I'm currently using this method to animate ~60 views around the screen simultaneously in a game.
First we're going to define the delegate protocol that our wrapper will call to notify of update events.
// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
func displayWillUpdate(deltaTime: CFTimeInterval)
}
Next we're going to define our display link wrapper class. This class will take a delegate reference on initialization. When initialized it will automatically start our display link, and clean it up on deinit.
import UIKit
class DisplayUpdateNotifier {
// **********************************************
// MARK: Variables
// **********************************************
/// A weak reference to the delegate/listener that will be notified/called on display updates
weak var listener: DisplayUpdateReceiver?
/// The display link that will be initiating our updates
internal var displayLink: CADisplayLink? = nil
/// Tracks the timestamp from the previous displayLink call
internal var lastTime: CFTimeInterval = 0.0
// **********************************************
// MARK: Setup & Tear Down
// **********************************************
deinit {
stopDisplayLink()
}
init(listener: DisplayUpdateReceiver) {
// setup our delegate listener reference
self.listener = listener
// setup & kick off the display link
startDisplayLink()
}
// **********************************************
// MARK: CADisplay Link
// **********************************************
/// Creates a new display link if one is not already running
private func startDisplayLink() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
displayLink?.add(to: .main, forMode: .commonModes)
lastTime = 0.0
}
/// Invalidates and destroys the current display link. Resets timestamp var to zero
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
lastTime = 0.0
}
/// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
#objc private func linkUpdate() {
// bail if our display link is no longer valid
guard let displayLink = displayLink else {
return
}
// get the current time
let currentTime = displayLink.timestamp
// calculate delta (
let delta: CFTimeInterval = currentTime - lastTime
// store as previous
lastTime = currentTime
// call delegate
listener?.displayWillUpdate(deltaTime: delta)
}
}
To use it you simply initialize an instance of the wrapper, passing in the delegate listener reference, then update your animations based on the delta time. In this example, the delegate passes the update call off to the animatable view (this way you could track multiple animating views and have each update their positions via this call).
class ViewController: UIViewController, DisplayUpdateReceiver {
var displayLinker: DisplayUpdateNotifier?
var animView: MoveableView?
override func viewDidLoad() {
super.viewDidLoad()
// setup our animatable view and add as subview
animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
animView?.configureMovement()
animView?.backgroundColor = .blue
view.addSubview(animView!)
// setup our display link notifier wrapper class
displayLinker = DisplayUpdateNotifier.init(listener: self)
}
// implement DisplayUpdateReceiver function to receive updates from display link wrapper class
func displayWillUpdate(deltaTime: CFTimeInterval) {
// pass the update call off to our animating view or views
_ = animView?.update(deltaTime: deltaTime)
// in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
// that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
// our view again
if animView?.isReadyForReuse == true {
animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
view.addSubview(animView!)
}
}
}
Our moveable views update function looks like this:
func update(deltaTime: CFTimeInterval) -> Bool {
guard canAnimate == true, isReadyForReuse == false else {
return false
}
// by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))
// update velocity with smoothed acceleration
velocity.adding(point: smoothAccel)
// update center with smoothed velocity
center.adding(point: smoothVel)
currentTime += 0.01
if currentTime >= timeLimit {
canAnimate = false
endAnimation()
return false
}
return true
}
If you'd like to look through a full demo project you can download it from GitHub here: CADisplayLink Demo Project
The above is the best example for how to use CADisplayLink with efficiency. Thanks to #Fattie and #digitalHound
I could not resist adding my use of CADisplayLink and DisplayUpdater classes by 'digitalHound' in PdfViewer using WKWebView.
My requirement was to continue auto scroll the pdf at the user selectable speed.
May be the answer here is not correct place, but I intent to show usage of CADisplayLink here. ( for others like me, who can implement their requirement. )
//
// PdfViewController.swift
//
import UIKit
import WebKit
class PdfViewController: UIViewController, DisplayUpdateReceiver {
#IBOutlet var mySpeedScrollSlider: UISlider! // UISlider in storyboard
var displayLinker: DisplayUpdateNotifier?
var myPdfFileName = ""
var myPdfFolderPath = ""
var myViewTitle = "Pdf View"
var myCanAnimate = false
var mySlowSkip = 0.0
// 0.125<=slow, 0.25=normal, 0.5=fast, 0.75>=faster
var cuScrollSpeed = 0.25
fileprivate var myPdfWKWebView = WKWebView(frame: CGRect.zero)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = myViewTitle
let leftItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(PdfViewController.PdfBackClick))
navigationItem.leftBarButtonItem = leftItem
self.view.backgroundColor = UIColor.white.cgColor
mySpeedScrollSlider.minimumValue = 0.05
mySpeedScrollSlider.maximumValue = 4.0
mySpeedScrollSlider.isContinuous = true
mySpeedScrollSlider.addTarget(self, action: #selector(PdfViewController.updateSlider), for: [.valueChanged])
mySpeedScrollSlider.setValue(Float(cuScrollSpeed), animated: false)
mySpeedScrollSlider.backgroundColor = UIColor.white.cgColor
self.configureWebView()
let folderUrl = URL(fileURLWithPath: myPdfFolderPath)
let url = URL(fileURLWithPath: myPdfFolderPath + myPdfFileName)
myPdfWKWebView.loadFileURL(url, allowingReadAccessTo: folderUrl)
}
//MARK: - Button Action
#objc func PdfBackClick()
{
_ = self.navigationController?.popViewController(animated: true)
}
#objc func updateSlider()
{
if ( mySpeedScrollSlider.value <= mySpeedScrollSlider.minimumValue ) {
myCanAnimate = false
} else {
myCanAnimate = true
}
cuScrollSpeed = Double(mySpeedScrollSlider.value)
}
fileprivate func configureWebView() {
myPdfWKWebView.frame = view.bounds
myPdfWKWebView.translatesAutoresizingMaskIntoConstraints = false
myPdfWKWebView.navigationDelegate = self
myPdfWKWebView.isMultipleTouchEnabled = true
myPdfWKWebView.scrollView.alwaysBounceVertical = true
myPdfWKWebView.layer.backgroundColor = UIColor.red.cgColor //test
view.addSubview(myPdfWKWebView)
myPdfWKWebView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor ).isActive = true
myPdfWKWebView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myPdfWKWebView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myPdfWKWebView.bottomAnchor.constraint(equalTo: mySpeedScrollSlider.topAnchor).isActive = true
}
//MARK: - DisplayUpdateReceiver delegate
func displayWillUpdate(deltaTime: CFTimeInterval) {
guard myCanAnimate == true else {
return
}
var maxSpeed = 0.0
if cuScrollSpeed < 0.5 {
if mySlowSkip > 0.25 {
mySlowSkip = 0.0
} else {
mySlowSkip += cuScrollSpeed
return
}
maxSpeed = 0.5
} else {
maxSpeed = cuScrollSpeed
}
let scrollViewHeight = self.myPdfWKWebView.scrollView.frame.size.height
let scrollContentSizeHeight = self.myPdfWKWebView.scrollView.contentSize.height
let scrollOffset = self.myPdfWKWebView.scrollView.contentOffset.y
let xOffset = self.myPdfWKWebView.scrollView.contentOffset.x
if (scrollOffset + scrollViewHeight >= scrollContentSizeHeight)
{
return
}
let newYOffset = CGFloat( max( min( deltaTime , 1 ), maxSpeed ) )
self.myPdfWKWebView.scrollView.setContentOffset(CGPoint(x: xOffset, y: scrollOffset+newYOffset), animated: false)
}
}
extension PdfViewController: WKNavigationDelegate {
// MARK: - WKNavigationDelegate
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//print("didStartProvisionalNavigation")
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("didFinish")
displayLinker = DisplayUpdateNotifier.init(listener: self)
myCanAnimate = true
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
//print("didFailProvisionalNavigation error:\(error)")
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
//print("didFail")
}
}
Example Calling from another view is as under.
To load the PDF file from Document folder.
func callPdfViewController( theFileName:String, theFileParentPath:String){
if ( !theFileName.isEmpty && !theFileParentPath.isEmpty ) {
let pdfViewController = self.storyboard!.instantiateViewController(withIdentifier: "PdfViewController") as? PdfViewController
pdfViewController?.myPdfFileName = theFileName
pdfViewController?.myPdfFolderPath = theFileParentPath
self.navigationController!.pushViewController(pdfViewController!, animated: true)
} else {
// Show error.
}
}
This example may be 'modified' to load web page and auto-scroll them at user selected speed.
Regards
Sanjay.

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
}
}