SKAction completion handler reset upon additional calls? - swift

I have some animations timed to audio using the completion handler in playSoundFileNamed - currently if I call it multiple times the animation actions are removed at the end of the first audio - so the audio from the additional calls continues but the animations stops (which makes sense) I have tried a variety of checks/if/else/etc but cannot figure out a way to have it keep animating as long as the audio is playing - w/o going to a delegate and using AVplayer which doesn't work as well for multiple sounds at once - IIRC...
example
//Animation
func animateBass() {
let danceAction = SKAction.repeatForever(SKAction.animate(with: animatedBass, timePerFrame: 0.1, resize: false, restore: true))
bass.run(danceAction)
}
//Action
let randomBass = bassLick.randomElement()
let bassAction = SKAction.playSoundFileNamed(randomBass!, waitForCompletion: true)
//Bass call
animateBass()
bassPlayer.run(bassAction) { [weak self] in
self?.bassPlayer.removeAllActions()
}
this works great if I only call it once but hoping to smash the button a million times and have the animation continue until the audio is truly finished!
Thanks!!

Related

Swift: How do I play a sound after exactly x seconds to show that a timer has finished?

I am currently working on a workout plan app. Part of the app is an exercise timer that will count up or down (this works). Additionally a sound should be played as soon as the timer finishes. Ideally that sound would not interfere with music being played, no volume changes, no pause, nothing. Just play a loud sound while music keeps playing. I have tried two ways to do that, both have critical drawbacks.
First solution:
I tried using an AVAudioPlayer that does play the alarm sound after x seconds, with x being the time set for the timer. It does work sometimes as expected, but sometimes the sound will play early. If the timer is set to 60 seconds, the alarm might play after 60 seconds, but sometimes after 55-57 seconds. The alarm is implemented like this:
private let audioPlayer: AVAudioPlayer
// init audioPlayer
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(error)
}
do {
audioPlayer = try AVAudioPlayer(contentsOf: alarmSound)
} catch {
audioPlayer = AVAudioPlayer()
}
audioPlayer.play(atTime: audioPlayer.deviceCurrentTime + timeTarget)
My second solution that always fires at the right time is using a userNotification. This approach has different drawbacks. The notification will not show if my app is in foreground and the sound will not play if the phone is in silent mode. Aditionally, it will interfere with music being played and show a banner, but I want my alert to be as less intrusive as possible. It is implemented like this:
private var notificationCenter: UNUserNotificationCenter
private var alarmMessage: UNMutableNotificationContent
notificationCenter = UNUserNotificationCenter.current()
alarmMessage = UNMutableNotificationContent()
alarmMessage.title = "Workout Plan"
alarmMessage.subtitle = "Exercise Timer"
alarmMessage.categoryIdentifier = "alarm"
alarmMessage.sound = UNNotificationSound(named: UNNotificationSoundName("dummy.wav"))
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeTarget, repeats: false)
let identifier = "exerciseTimer"
let request = UNNotificationRequest(identifier: identifier, content: alarmMessage, trigger: trigger)
notificationCenter.add(request)
How do I play a sound at a specific time without any offsets or annoying the user with useless notifications?

Update Showing Frame of AVPlayer When Using AVPlayer.seek() Prior to AVPlayer.play()

I have an AVPlayer and when I get to the end of the duration, I want to go back to the beginning, but I don't want to restart the AVPlayer.
//Do something when video ended
NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(note:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
#objc func playerDidFinishPlaying(note: Notification) {
self.restartVideo()
}
func restartVideo() {
self.player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero)
}
From here, I can simply .play() the AVPlayer and it will play from the beginning. But, prior to me .play(), the .seek() function does not indicate to the user that it has been reset back to the beginning. The AVPlayer frame does not actually transition back to the first frame until AFTER I execute .play().
Question:
How can I get the AVPlayer to change the actual frame it shows to the user when using the .seek() functionality?
This part of your code is right. It could be something else calling it to seek to end??
You can use the completion handler to see if the seek was interrupted. And also set a break point to see what queue you're on, just to make sure you're on the main thread. If a second seek operation happens you will get a false result in the completion handler. Also, try setting a break point after the seek to check if the UI responded.
self.player.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) { success in
print("seek finished success = ", success)
}
set like this
self.player.seek(to: .zero) // it will update the player slider to 0:00
if you want to set on specific time than use it like
// Seek to the 2 minute mark
let time = CMTime(value: 120, timescale: 1)
self.player.seek(to: time)

How to get current length of AVCaptureMovieFileOutput while recording to display on screen?

As title says, I am looking to display the current length of the recording while a user is recording video with AVFoundation. I have done something similar with AVAudioRecorder where I create a timer that sets a label's text as follows:
recordDurationLabel.text = timeString((audioRecorder?.currentTime)!)
I was wondering if there is a "currentTime" feature for recording video? My only other thought would be to start a repeated 1 second timer in the captureOutput:didStartRecordingToOutputFileAtURL function but I want to ensure accuracy.
Thanks in advance!
Edit:
For now this is the function I've implemented in the captureOutput:didStartRecordingToOutputFileAtURL delegate method, but again, would be cleaner to have something directly related to the video.
func startRecordingUpdateTimer() {
recordingUpdateTimer = NSTimer(timeInterval: 1.0/45.0, target: self, selector: #selector(recordedVideo), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(recordingUpdateTimer!, forMode: NSRunLoopCommonModes)
}
func recordedVideo() {
durationTime = durationTime + 1.0/45.0
let durationString = timeString(durationTime)
recordDurationLabel.text = durationString
}
With an AVCaptureMovieFileOutput, you can get the duration of the movie by using recordedDuration which is read only and will return you a CMTime when the recording has started. See AVCaptureFileOutput.
If you use captureOutput:didOutputSampleBuffer:fromConnection: you'll receive CMSampleBufferobjects which contain a CMTimethat you can access with CMSampleBufferGetPresentationTimeStamp.
Doing a CMTime operation like CMTimeSubstract will allow you to calculate the time difference between the current buffer and the first one you recorded.
If you have never manipulated sampleBuffers, you should take a look at this example code from Apple: RosyWriter

How to run timer and AI logic in Swift simultaneously

I have an NSTimer() and an AI logic for a board game. The AI logic takes long time to process about 3 to 5 seconds(which is ok). When the program is executing the AI logic, the NSTimer doesn't fire until the AI logic finished it's execution.
This is how i started the timer during the initial stage of the game.
public var timer = NSTimer()
...
let timeSelector:Selector = "timerFired"
if self.useTime {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: timeSelector, userInfo: nil, repeats: true)
}
I added the AI logic in Update of an SKScene so that when it detects it is his turn, he will start checking for available moves
override func update(currentTime: CFTimeInterval) {
if gameStatus.playerTurn == "AI" && !aiThiking {
aiThiking = true
aIPlayer.aiCheckAvaialbleMoves()
// executeMove
// change Turn
aiThiking = false
}
My question is, is it possible to let the AI logic execute and let the timer still running at the same time?
Your problem seems to be that aIPlayer.aiCheckAvailableMoves() freezes the app as long as it takes to execute, so NSTimer does not work as it should. You can use different threads to avoid this:
dispatch_async(dispatch_get_global_queue(priority, 0)) {
aIPlayer.aiCheckAvaialbleMoves()
}
This code runs this function in a new thread and when the code ended, the thread is closed automatically.
You could use completion blocks to know exactly when the function ended. Here is an example:
func aiCheckAvailableMoves(completion : (result : BOOL) -> Void) {
dispatch_async(dispatch_get_global_queue(priority, 0)) {
//Do your code normally
//When you have finished, call the completion block (like a return)
completion(true)
}
}
And then, from your update: you will get when the function ended (please note here there is no dispatch_async):
aIPlayer.aiCheckAvailableMoves(){
//Finished checking moves, maybe schedule again?
}
Be careful with threads, as they can cause unwanted behaviour or/and crashes!

can AVAudioPlayer trigger events on predefined intervals?

I need to make views visible at fixed time while running a MP3 audio. I am using Swift. Any suggestions?
I don't like using timers if drawing depends on them. They may fire more frequently than the screen redraws or out of sync with the screen drawing. I use a CADisplayLink instead. It's similar to a timer but it is designed to fire in synch with the screen's refresh rate. You could implement something like this.
var messagePlaybackTimer: CADisplayLink?
var player: AVAudioPlayer?
func playAudioData(){
let path = NSBundle.mainBundle().pathForResource("audio", ofType: "mp3")
var url: NSURL
if let audioResourcePath = path {
url = NSURL.fileURLWithPath(audioResourcePath)!
}else{
return;
}
player = AVAudioPlayer(contentsOfURL: url, error: nil)
messagePlaybackTimer?.invalidate()
messagePlaybackTimer = nil;
messagePlaybackTimer = CADisplayLink(target: self, selector: Selector("messagePlaybackTimerFired"))
messagePlaybackTimer?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
player?.play()
}
func messagePlaybackTimerFired(){
println(player?.currentTime)
}
In messagePlaybackTimerFired() you can check the AVAudioPlayer's current time and fire events based on that. (E.g. if player?.currentTime > 5 && player?.currentTime < 6 { do something } )
This will allow you to fire the method at as close to 5 seconds as possible while still being optimal for doing UI work.
When you are done with the CADisplayLink (when the player stops playing) be sure to invalidate it. If you don't, it will keep firing when you don't need it anymore.
Another thing you can do is fire off an NSNotification in messagePlaybackTimerFired() and your UIViewController can subscribe to the notification and show the views when appropriate.
Use an NSTimer or the AVAudioPlayer's addPeriodicTimeObserverForInterval:queue:usingBlock: method to check the value, it's (very most likely) not possible to get updates when a certain time is reached.
Oh and for a Swiftier NSTimer I suggest this