Play audio file once every time NSTimer decrements? - ios

I'm working in Swift and I have an NSTimer that counts down from 3, 2, 1. I want to play an audio file every time this timer decrements, so that the timer would show 3 and the audio would play once, then it flips to 2 and the audio plays once, finally it goes to 1 and it plays once.
This is how I've created the timer and tried to do that:
var path2 = NSBundle.mainBundle().pathForResource("splatter1", ofType: "wav")
var soundTrack2 = AVAudioPlayer()
func timeToMoveOn() {
let loadingDelay = 0.01 * Double(NSEC_PER_SEC)
let loadingTime = dispatch_time(DISPATCH_TIME_NOW, Int64(loadingDelay))
dispatch_after(loadingTime, dispatch_get_main_queue()) {
//After delay
self.performSegueWithIdentifier("goToGameScene", sender: self)
}
}
func splatterSound() {
soundTrack2 = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path2!), error: nil)
soundTrack2.numberOfLoops = 1
soundTrack2.volume = 0.35
soundTrack2.play()
}
func updateCounter() {
countdownTestLabel.text = String(counter--)
splatterSound()
if counter == 0 {
countdown.invalidate()
//Then trigger segue to Game Scene view
timeToMoveOn()
}
}
In view did load:
counter = 3
countdown = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: Selector("updateCounter"), userInfo: nil, repeats: true)
My problem is that the audio file plays every time the timer decrements, but it plays multiple times. For example it will one time, then a second time, then a third fourth and fifth time all in rapid succession. It shouldn't do that.
How can I fix this so that it only plays when the timer decrements?

Get rid of the soundTrack2.numberOfLoops = 1 bit. The numberOfLoops setting is the number of times to repeat the sound. So the first time your timer fires, you queue up your sound to play twice in a row.
Then one second later, you queue up the sound to play twice in a row again.
Then, yet one second later, you queue up your sound to play another twice in a row.
If you eliminate that line, the sound will default to numberOfLoops=0, or just playing the sound once (which is what you want.)

Related

Intermittent Missing Seconds with Timer

I have a timer, which is a singleton, that repeatedly fires every second. I allow the user to pause the timer as well as resume. I am keeping track of the start date of the timer, and I am subtracting any pauses from the elapsed time.
Unfortunately, I can't seem to fix an intermittent issue where pausing and resuming the timer causes a skipping of one second.
For instance, in the following code block, I start the timer and print the seconds:
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
11.0
12.0
13.0
14.0
15.0
16.0
17.0
18.0
19.0
In the following code block, I resume the timer, printing the seconds again. However, as you can see, the 20th second has not been printed:
21.0
22.0
23.0
24.0
25.0
26.0
I cannot seem to figure out where I am losing the second. It does not happen with each pause and resume cycle.
The properties that I am using to keep track of the aforementioned are as follows:
/// The start date of the timer.
private var startDate = Date()
/// The pause date of the timer.
private var pauseDate = Date()
/// The number of paused seconds.
private var paused = TimeInterval()
/// The number of seconds that have elapsed since the initial fire.
private var elapsed = TimeInterval()
I start the timer by creating the timer and setting the start date:
/// Starts the shower timer.
func startTimer() {
// Fire the timer every second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
// Set the start time of the initial fire.
startDate = Date()
}
If the user pauses the timer, then the following method executes:
/// Pauses the shower timer.
func pauseTimer() {
// Pause the timer.
timer?.invalidate()
// Set the timer to `nil`, according to the documentation.
timer = nil
// Set the date of the pause.
pauseDate = Date()
}
Then, the following method executes when the user resumes the timer:
/// Resumes the timer.
func resumeTimer() {
// Recreate the timer.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
// Add the number of paused seconds to the `paused` property.
paused += Date().timeIntervalSince(pauseDate)
}
The following method, which is called by the method that executes when the timer fires, sets the number of elapsed seconds since the initial fire, less the sum of any pauses:
/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
// Get the date for now.
let now = Date()
// Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
elapsed = now.timeIntervalSince(startDate).rounded(.down).subtracting(paused.rounded(.down))
}
Finally, the following method is the Selector that executes when the timer fires:
/// Updates the number of elapsed seconds since the timer has been firing.
#objc private func updateElapsedSeconds() {
// Configure the elapsed time with each fire.
updateElapsedTime()
// Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}
What am I doing incorrectly to cause a missing second intermittently?
So the issue here is that Timer is not accurate in this way. Or rather, its timekeeping is reasonably accurate, but the actual rate of firing has some variance as it is dependent on the runloop.
From the documentation:
A timer is not a real-time mechanism; it fires only when one of the
run loop modes to which the timer has been added is running and able
to check if the timer’s firing time has passed.
To show this, I got rid of all of the rounding in your code and printed the output (you don't even need to pause to see this happen). Here is what this variance looked:
18.0004420280457
19.0005180239677
20.0004770159721
21.0005570054054
21.9997390508652
23.0003360509872
24.0003190040588
24.9993720054626
25.9991790056229
Sometimes it fires particularly late and this causes the whole thing to get thrown off. The rounding doesn't help because you are still depending on the timer for the actual reference time and eventually it will be off by more than a second.
There are a few ways to fix the situation here depending on what exactly you are trying to accomplish. If you absolutely need the actual time, you can adjust the timer to fire at fractions of a second and instead use that output to estimate the seconds a little more accurately. This is more work and will still not be totally right (there will always be a variance).
Based on your code, it seems like simply incrementing a number based on the timer should be enough to accomplish your goal. Here is a simple modification to your code making this work. This will count up simply and never skip a second in the count whether you pause or not:
/// The number of seconds that have elapsed since the initial fire.
private var elapsed = 0
private var timer: Timer?
/// Starts the shower timer.
func startTimer() {
elapsed = 0
// Fire the timer every second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}
/// Pauses the shower timer.
func pauseTimer() {
// Pause the timer.
timer?.invalidate()
// Set the timer to `nil`, according to the documentation.
timer = nil
}
/// Resumes the timer.
func resumeTimer() {
// Recreate the timer.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}
/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
// Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
elapsed += 1
// debug print
print(elapsed)
}
/// Updates the number of elapsed seconds since the timer has been firing.
#objc private func updateElapsedSeconds() {
// Configure the elapsed time with each fire.
updateElapsedTime()
// Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}

Swift - Best way to run timer in background for iphone apps

I'm totally new to iOS development and am working on an iPhone cooking app that gives the user the choice of three 'timer' options. The first timer runs for 6 mins, the second for 8.5 mins and the last for 11 mins.
Once the timer finishes counting down it plays an audio file and displays a message within the app screen. Everything works perfectly, except that I've discovered in testing that the timer stops running while the user goes to another app (e.g. checking email, using Safari, etc). Obviously, this defeats the purpose of the app as the user needs to know when the timer is finished so they can do the next step (e.g. remove a saucepan from the stove).
I've researched background modes and am getting confused. It seems that I literally have no reason (according to Apple) to run this app in the background (i.e. it's not playing music, using locations services, etc). I also keep reading that there's a 10 min limit to running in the background otherwise.
I also come across the idea of local and remote notifications, but the page I was referred to no longer exists on Apple's developer site. I'm now at a loss and confused.
Is there a way for me to actually get this app to work in the background for up to 11 minutes? If so, how?
Here's an update. I've been trying to get my head around Local Notifications and Background Tasks.
LOCAL NOTIFICATIONS
This showed some promise, but I'm not sure how I would implement this in my scenario? How would I ensure the right amount of time passes before the notification appears/plays a sound?
For example, the user selects the button for 'soft boiled eggs' at exactly 12:00:00pm and the app starts a counter for 6 mins. At 12:01:20pm the user reads an email, taking 30 seconds before putting the phone down at 12:01:50 to read the paper. Let's assume at 12:02:50 the phone goes into lock mode, how do I ensure the local notification triggers 3mins and 10secs later to make up the whole 6mins and play the sound file notifying the user their eggs are ready.
BACKGROUND TASKS
This may work for my scenario if I can start and restart background tasks to allow my timer to complete before playing the sound.
Below is a snippet of my code (relating to the eggs example above) that I hope will help put my app in context:
#IBAction internal func ButtonSoft(sender: UIButton) {
counter = 360
TimerDisplay.text = String("06:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonMedium(sender: UIButton) {
counter = 510
TimerDisplay.text = String("08:30")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonHard(sender: UIButton) {
counter = 660
TimerDisplay.text = String("11:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
func stopTimer() {
if counter == 0 {
timer.invalidate()
}
}
func updateCounter() {
counter--
let seconds = counter % 60
let minutes = (counter / 60) % 60
let strMinutes = minutes > 9 ? String(minutes) : "0" + String(minutes)
let strSeconds = seconds > 9 ? String(seconds) : "0" + String(seconds)
if seconds > 0 {
TimerDisplay.text = "\(strMinutes):\(strSeconds)"
}
else {
stopTimer()
TimerDisplay.text = String("Eggs done!!")
SoundPlayer.play()
}
}
#IBAction func ButtonReset(sender: AnyObject) {
timer.invalidate()
stopTimer()
TimerDisplay.text = String("Choose your eggs:")
ButtonSoft.alpha = 1.0
ButtonMedium.alpha = 1.0
ButtonHard.alpha = 1.0
ButtonSoft.enabled = true
ButtonMedium.enabled = true
ButtonHard.enabled = true
}
In terms of running background tasks, I've come across the following example of code.
To create the background task:
func someBackgroundTask(timer:NSTimer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
println("do some background task")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
println("update some UI")
})
})
}
And the below line of code to (I think) use a timer to run the above function:
var timer = NSTimer(timeInterval: 1.0, target: self, selector: "someBackgroundTask:", userInfo: nil, repeats: true)
And the below code to stop it all:
timer.invalidate()
So, how would I adapt this for my scenario? If this isn't possible, how would I use local notifications in my app?
Or do I just give up on the iPhone version of this app (my Apple Watch version seems to work fine).
In a word, no. You can ask for background time, but recent versions of iOS give you 3 minutes.
If you are a background sound playing app or navigation app you are allowed to run in the background for longer, but you have to ask for those permissions and the app review board will check.
The bottom line is that third parties can't really do a timer app that counts down an arbitrary time longer than 3 minutes.
You might want to use timed local notifications. You can make those play a sound when they go off. Search in the Xcode docs on UILocalNotification.
Edit in Aug 2020: I would no longer recommend this approach.
I have had some success by starting a background task, then setting a timer for just under a minute. When the timer fires, I start a new background task and end the old one. I just keep rolling over the background task, creating a new one every minute.

Detect how long a sound is in iOS

I'm using the audio metering in AVFoundation and I wanted to know if there's a way to figure out how long a sound is. I have my audio recorder setup like this:
func startMeterTimer() {
levelTimer?.invalidate()
levelTimer = CADisplayLink(target: self, selector: "updateMeter")
levelTimer?.frameInterval = 5
levelTimer?.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
func stopMeterTimer() {
levelTimer?.invalidate()
levelTimer = nil
}
func updateMeter() {
readLevels()
let avgPower = audioRecorder?.averagePowerForChannel(0)
let peakPower = audioRecorder?.peakPowerForChannel(0)
if peakPower >= -2 {
print("CLAP DETECTED")
soundLogicDelegate?.clapDetected()
}
// print("\(avgPower) + \(peakPower)")
}
func record() -> Bool {
return (audioRecorder?.record())!
}
record and startMeterTImer are called and when I clap my hand, even though I only clap once, I get many print statements of CLAP DETECTED. I was wondering if there was a way to measure how long a method is called from the first time it's called and the last time it's called.
Use the audioRecorderDidFinishRecording delegate method, one of it's parameters is an instance of AVAudioRecorder which has currentTime property:
The time, in seconds, since the beginning of the recording.
(read-only)
If you want to check recording time each time updateMeter() is called use audioRecorder?.currentTime

NSTimer stops when view controller is not the selected tab or not showing

I have a strange problem with my countdown timer. It fires off normally when my start button is hit, and is reinstantiated correctly when I close the app and relaunch it again. However, when I select a different tab and stay there for a while, it stops counting down, then resumes counting down from where it left off when I show the countdown tab again.
For example, if the timer is now at 00:28:00 (format is HH:MM:SS), select some other tab, stay there for 5 minutes, and then go back to the timer tab, it's only at the 27:52 mark. When I close the app (double tap the home button, swipe up my app) and reopen it, it starts off at a more reasonable 22:50 mark.
I've posted the relevant code from the class to show how I'm setting up the timer, but a summary of what it does:
I have plus (+) and minus (-) buttons somewhere that, when tapped, call recalculate().
recalculate() fires off a CalculateOperation.
A CalculateOperation computes for the starting HH:MM:ss based on the addition/removal of a new record. The successBlock of a CalculateOperation executes in the main thread.
A CalculateOperation creates the NSTimer in the successBlock if the countdownTimer hasn't been created yet.
The NSTimer executes decayCalculation() every 1 second. It reduces the calculation.timer by 1 second by calling tick().
Code:
class CalculatorViewController: MQLoadableViewController {
let calculationQueue: NSOperationQueue // Initialized in init()
var calculation: Calculation?
var countdownTimer: NSTimer?
func recalculate() {
if let profile = AppState.sharedState.currentProfile {
// Cancel all calculation operations.
self.calculationQueue.cancelAllOperations()
let calculateOperation = self.createCalculateOperation(profile)
self.calculationQueue.addOperation(calculateOperation)
}
}
func decayCalculation() {
if let calculation = self.calculation {
// tick() subtracts 1 second from the timer and adjusts the
// hours and minutes accordingly. Returns true when the timer
// goes down to 00:00:00.
let timerFinished = calculation.timer.tick()
// Pass the calculation object to update the timer label
// and other things.
if let mainView = self.primaryView as? CalculatorView {
mainView.calculation = calculation
}
// Invalidate the timer when it hits 00:00:00.
if timerFinished == true {
if let countdownTimer = self.countdownTimer {
countdownTimer.invalidate()
}
}
}
}
func createCalculateOperation(profile: Profile) -> CalculateOperation {
let calculateOperation = CalculateOperation(profile: profile)
calculateOperation.successBlock = {[unowned self] result in
if let calculation = result as? Calculation {
self.calculation = calculation
/* Hide the loading screen, show the calculation results, etc. */
// Create the NSTimer.
if self.countdownTimer == nil {
self.countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("decayCalculation"), userInfo: nil, repeats: true)
}
}
}
return calculateOperation
}
}
Well, if I leave the app in some other tab and not touch the phone for a while, it eventually goes to sleep, the app resigns active, and enters the background, which stops the timer.
The solution was to set my view controller as a listener to the UIApplicationWillEnterForegroundNotification and call recalculate to correct my timer's countdown value.

Swift - Have an audio repeat, but it plays in intervals?

In Swift, I play a song, it plays at the viewDidLoad. It is 16 seconds long, and it plays all 16 seconds. I want it to repeat forever. I made an NSTimer where every 16 seconds, it plays the song. But, it plays 16 seconds when the app loads, stops for 16 seconds, plays, etc.
The line println("just doing some dandy debugging here.") does print every 16 seconds.
How is this fixed?
CODE:
//these var's are created on the top of the file.
var soundTimer: NSTimer = NSTimer()
var audioPlayer2 = AVAudioPlayer()
var soundTwo = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("sound", ofType: "wav"))
//this stuff is in the viewDidLoad function.
audioPlayer2 = AVAudioPlayer(contentsOfURL: soundTwo, error: nil)
audioPlayer2.prepareToPlay()
audioPlayer2.play()
soundTimer = NSTimer.scheduledTimerWithTimeInterval(16, target: self, selector: Selector("soundTimerPlayed"), userInfo: nil, repeats: true)
func soundTimerPlayed() {
println("just doing some dandy debugging here.")
audioPlayer2.stop()
audioPlayer2.prepareToPlay()
audioPlayer2.play()
}
Just do it the easy, official way, instead. From the documentation of the numberOfLoops property:
Set any negative integer value to loop the sound indefinitely until you call the stop method.
But your actual problem is almost certainly because you're stopping playback a little early:
The stop method does not reset the value of the currentTime property to 0. In other words, if you call stop during playback and then call play, playback resumes at the point where it left off.
What's happening is that you're stopping the sound playback just a little before the actual end of the sample—your timer is set to just too short a time. The first time the timer is triggered, the "play" is playing the tiny amount of quiet or silent sound that's left at the end, and then stopping. The next time, the currentTime has been reset because the sample has successfully reached the end of playback, so on the next timer interval, after another 16 seconds, playback starts successfully from the beginning. And repeat.
As Jack observes in his comment, this is why there's a delegate callback to give you a kick when the playback has actually finished, but I'd still just set numberOfLoops and not bother with the complicated stuff, if you only need the sound to loop indefinitely.
(If you desperately want to repeat on your exact timer event, then just set currentTime to 0 before you play it again.)

Resources