AVPlayer fadeIn - swift

I have been fighting with this to try an implement a fadeIn method for the AVPlayer. I even tried making an extension, but I don't know how to attach it to the AVPlayer.
I don't know why this doesn't work. I can see print statements that the audioPlayer volume is being adjusted up from zero to full volume, but I do not hear any volume until the function exits and then it is at full volume, with no fadeIn.
I'm working in a playground:
import Cocoa
import AVFoundation
let url = URL(fileURLWithPath: "Sample.mp3")
func fadeIn(player: AVPlayer, fadeIn: Float) {
let volumeInc = Float(1.0 / 10.0)
let fadeInc = Float(fadeIn / 10.0)
let sleepTime = useconds_t(fadeInc * 1000000)
var seconds = Float(0.0)
while seconds < fadeIn {
usleep (sleepTime)
print("gainInc (\(volumeInc)) + player.volume (\(player.volume)) = \(volumeInc + player.volume)")
if (volumeInc + player.volume) > 1.0 {
print("Breaking out of the while loop.")
break
} else {
print("Incrementing the player.volume (\(player.volume)) by gainInc (\(volumeInc))")
player.volume += volumeInc
seconds += fadeInc
}
}
}
let player = AVPlayer(url: url)
player.seek(to: CMTimeMakeWithSeconds(45, preferredTimescale: 60000))
player.volume = 0
player.play()
fadeIn(player: player, fadeIn: 2)
It appears AVPlayer is being blocked because even if I forego the method call and just put in
let player = AVPlayer(url: url)
player.seek(to: CMTimeMakeWithSeconds(45, preferredTimescale: 60000))
player.volume = 0
player.play()
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
usleep(200000)
player.volume += 0.1
The player is still being blocked. I then went a step further and tried
let player = AVPlayer(url: url)
player.seek(to: CMTimeMakeWithSeconds(45, preferredTimescale: 60000))
player.volume = 1
player.play()
usleep(2000000)
And the player is still blocked. I haven't read anything that requires AVPlayer to be in it's own thread, so is this behavior correct? Do I need crawl out of this rabbit hole to find another to go down?
EDIT: So, I implemented the answer and I thought I had that viola moment in that I could then monitor any key presses with a play timer and using nested timers to fade in or fade out based upon key presses while keeping track of the play time. However, trying to implement the answer even further, I am once again experiencing a blocking action when trying to timer to fadeItIn within the playTimer timer.
I'm basically wanting to create an app that acts like a radio station scanner to step through my media library and fade a song in over a 2 second period and play for 10 seconds (probably 10, but I use 25 for testing), and if I like it and want to hear more I can click the "Continue playing to end" button. However, if I don't click that button, or pause/stop/next track buttons, the player ends with a 2 second fade out and goes on to the next track in my media library.
Updated code:
import Cocoa
import AVFoundation
import PlaygroundSupport
let url = URL(fileURLWithPath: "Sample.mp3")
let startTime = 45
let fadeIn = 2
let fadeOut = 2
let playTime = 25
let player = AVPlayer(url: url)
let maxVolume:Float = 1.00
var toEnd = false
var pausePlayer = false
var nextTrack = false
var stopPlayer = false
var timePlayed = 0
var playerVolume:Float = 0.00
player.seek(to: CMTimeMakeWithSeconds(Float64(startTime), preferredTimescale: 60000))
player.volume = 0
player.play()
print("player maxVolume = \(maxVolume)")
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { playTimer in
let currentTime = Int(player.currentTime().seconds-Double(startTime))
if currentTime > timePlayed {
timePlayed += 1
print("Current play time = \(currentTime)")
}
if currentTime == 0 {
Timer.scheduledTimer(withTimeInterval: Double(fadeIn/10), repeats: true) { fadeItIn in
print("player.volume before fadeIn adjustment = \(player.volume)")
if playerVolume < maxVolume {
// Doing this to prevent volume from exceeding 1.00
playerVolume += 0.05
player.volume = playerVolume
print("player.volume after fadeIn adjustment = \(player.volume)")
}
if playerVolume >= maxVolume {
print("fadeIn volume has reached maximum volume at \(player.volume), invalidating faderItIn")
fadeItIn.invalidate()
}
}
}
if currentTime >= playTime-fadeOut {
playerVolume = player.volume
Timer.scheduledTimer(withTimeInterval: Double(fadeOut/10), repeats: true) { fadeItOut in
print("player.volume before fadeOut adjustment= \(player.volume)")
if playerVolume > 0.00 {
// Doing this to prevent the volume from going negative
playerVolume -= 0.05
player.volume = playerVolume
print("player.volume after fadeOut adjustment = \(player.volume)")
}
if player.volume <= 0 {
print("fadeOut volume has reached minimum volume at \(player.volume), invalidating fadeItOut")
fadeItOut.invalidate()
}
}
print("Pausing player")
player.pause()
print("Setting player item to nil")
player.replaceCurrentItem(with: nil)
print("Invalidating playTimer")
playTimer.invalidate()
} else if pausePlayer && player.timeControlStatus.rawValue == 1 {
player.pause()
} else if toEnd || nextTrack || stopPlayer && player.timeControlStatus.rawValue == 1 {
playTimer.invalidate()
if stopPlayer || nextTrack {
player.replaceCurrentItem(with: nil)
}
}
}
if toEnd {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { playTimer in
let currentTime = Int(player.currentTime().seconds-Double(startTime))
if currentTime > timePlayed {
print("Play time: \(currentTime)")
timePlayed += 1
}
if pausePlayer && player.rate > 0 {
player.pause()
} else if !pausePlayer && player.rate == 0 {
player.play()
}
if stopPlayer || nextTrack {
player.pause()
player.replaceCurrentItem(with: nil)
playTimer.invalidate()
}
}
}

AVPlayer is UI component that works on the main thread so with usleep you block the thread and that can cause crashes in real applications.
To implement your feature you should change the player's volume asynchronous for instance with Timer on the current run loop:
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { timer in
player.volume += 0.1
if player.volume >= 1 {
timer.invalidate()
}
}

Related

Fill the CALayer with different color when timer reaches to 5 seconds swift ios

In my app i am using a third party library which does circular animation just like in the appstore app download animation. i am using the external file which i have placed in my project. it works but when the timer reaches to 5 seconds the fill color should be red. Currently the whole layer red color applies, i want to only apply the portion it has progressed. i am attaching the video and the third party file. Please can anyone help me with this as what changes should i make to the library file or is there better solution
same video link in case google drive link doesnt work
External Library link which i am using
video link
external third file file
My code snippet that i have tried:
func startGamePlayTimer(color: UIColor)
{
if !isCardLiftedUp {
self.circleTimerView.isHidden = false
self.circleTimerView.timerFillColor = color
Constants.totalGamePlayTime = 30
self.circleTimerView.startTimer(duration: CFTimeInterval(Constants.totalGamePlayTime))
}
if self.gamePlayTimer == nil
{
self.gamePlayTimer?.invalidate()
let timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateGamePlayTime),
userInfo: nil,
repeats: true)
timer.tolerance = 0.05
RunLoop.current.add(timer, forMode: .common)
self.gamePlayTimer = timer
}
}
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7)
}
}
else
{
self.stopGamePlayTimer()
}
}
where circleTimerView is the view which animates as the time progresses
You can achieve that with CAKeyframeAnimation on the StrokePath.
I've Update the code in CircleTimer View as below. You can modified fill color and time as per your need.
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
// self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7) // <- Comment this line
}
}
else
{
self.stopGamePlayTimer()
}
}
open func drawFilled(duration: CFTimeInterval = 5.0) {
clear()
if filledLayer == nil {
let parentLayer = self.layer
let circleLayer = CAShapeLayer()
circleLayer.bounds = parentLayer.bounds
circleLayer.position = CGPoint(x: parentLayer.bounds.midX, y: parentLayer.bounds.midY)
let circleRadius = timerFillDiameter * 0.5
let circleBounds = CGRect(x: parentLayer.bounds.midX - circleRadius, y: parentLayer.bounds.midY - circleRadius, width: timerFillDiameter, height: timerFillDiameter)
circleLayer.fillColor = timerFillColor.cgColor
circleLayer.path = UIBezierPath(ovalIn: circleBounds).cgPath
// CAKeyframeAnimation: Changing Fill Color on when timer reaches 80% of total time
let strokeAnimation = CAKeyframeAnimation(keyPath: "fillColor")
strokeAnimation.keyTimes = [0, 0.25, 0.75, 0.80]
strokeAnimation.values = [timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = duration;
circleLayer.add(strokeAnimation, forKey: "fillColor")
parentLayer.addSublayer(circleLayer)
filledLayer = circleLayer
}
}
open func startTimer(duration: CFTimeInterval) {
drawFilled(duration: duration) // <- Need to pass duration here
if useMask {
runMaskAnimation(duration: duration)
} else {
runDrawAnimation(duration: duration)
}
}
UPDATE:
CAKeyframeAnimation:
You specify the keyframe values using the values and keyTimes properties.
For example:
let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorKeyframeAnimation.values = [UIColor.red.cgColor,
UIColor.green.cgColor,
UIColor.blue.cgColor]
colorKeyframeAnimation.keyTimes = [0, 0.5, 1]
colorKeyframeAnimation.duration = 2
This animation will run for the 2.0 duration.
We have 3 values and 3 keyTimes in this example.
0 is the initial point and 1 be the last point.
It shows which associated values will reflect at particular interval [keyTimes] during the animation.
i.e:
KeyTime 0.5 -> (2 * 0.5) -> At 1 sec during animation -> UIColor.green.cgColor will be shown.
Now to answer your question from the comments, Suppose we have timer of 25 seconds and we want to show some value for last 10 seconds, we can do:
strokeAnimation.keyTimes = [0, 0.25, 0.50, 0.60]
strokeAnimation.values = [timerFillColor.cgColor,timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = 25.0 // Let's assume duration is 25.0

Why is my animation quicker than the timer?

I have an animation that I want to coincide with the timer but right now it ends with 6seconds left of the timer. How do I get the animation to match? Also, how would i go about repeating the animation for the countdown of the iteration, i?
The code has the animation, in a circle, and then preset timer of 30s (which will eventually be a slider input). I will also eventually want to include a pause, and stop button for the timer which will need to coincide with the animation
import UIKit
var timer = Timer()
var time = 30
var i = 5
class ViewController: UIViewController {
#IBOutlet weak var displayTime: UILabel!
let shape = CAShapeLayer()
private let label: UILabel = {
let label = UILabel()
label.text = String(i)
// change label to update i
label.font = .systemFont(ofSize: 36, weight: .light)
return label
}()
func countdown() {
displayTime.text = String(time)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
}
override func viewDidLoad() {
super.viewDidLoad()
label.sizeToFit()
view.addSubview(label)
label.center = view.center
let circlePath = UIBezierPath(arcCenter: view.center, radius: 150, startAngle: -
(.pi / 2), endAngle: .pi * 2, clockwise: true)
let trackShape = CAShapeLayer()
trackShape.path = circlePath.cgPath
trackShape.fillColor = UIColor.clear.cgColor
trackShape.lineWidth = 15
trackShape.strokeColor = UIColor.lightGray.cgColor
view.layer.addSublayer(trackShape)
shape.path = circlePath.cgPath
shape.lineWidth = 15
shape.strokeColor = UIColor.blue.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.strokeEnd = 0
// cg = core graphics
view.layer.addSublayer(shape)
let button = UIButton(frame: CGRect(x: 20, y: view.frame.size.height-70, width:
view.frame.size.width-40, height: 50))
view.addSubview(button)
button.setTitle("Animate", for: .normal)
button.backgroundColor = .systemGreen
button.addTarget(self, action:#selector(didTapButton), for: .touchUpInside)
}
#objc func didTapButton() {
countdown()
// Animate
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.toValue = 1
animation.duration = Double(time)
// duration of animation
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
shape.add(animation, forKey: "animation")
}
#objc func doCountdown() {
time = time - 1
displayTime.text = String(time)
if time == 0 {
i = i - 1
time = 30
}
if i == 0 {
label.text = "0"
timer.invalidate()
}
}
}
Your implementation does not work because you are using a naive implementation of countdown.
A timer is not guaranteed to fire exactly after the given amount of time. It won't fire exactly after one second. The accuracy of Timer is 50-100 milliseconds. Therefore the total possible error can add up to 30*100 milliseconds, that is 3 entire seconds.
Instead, you have to use a Timer that will update your UI more often:
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(doCountdown), userInfo: nil, repeats: true)
And that also means you have to calculate your time differently. First of all, store the expected time of animation end:
// declare instance properties
private var animationEnd = Date()
private var timer: Timer? {
didSet {
// invalidate when nil is assigned
oldValue?.invalidate()
}
}
func startCountdown() {
// store the start time - 30 seconds in the future
animationEnd = Date().addingTimeInterval(TimerInterval(time))
// start the timer
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
guard let self = self else { return }
let remainingTime = max(0, self.animationEnd.timeIntervalSinceNow)
if remainingTime == 0 {
// invalidate the timer
self.timer = nil
}
// convert time to seconds
let remaininingSeconds = Int(remainingTime) ?? 0
self.displayTime.text = "\(remaininingSeconds)"
}
}
//
That's all.
If you want to pause & resume the timer, the process is the same. Invalidate the timer, store the current time (e.g. timePaused = Date) and when resumed, just add the difference between current time and timePaused to animationEnd and restart the timer.
Also, please, don't put variables on file level. Always put them to the scope of classes. Otherwise you will soon have problems.
I think these two variables are the source of your problem -
var time = 30
var i = 5
Can you try deleting the i variable and use this updated implementation -
#objc func doCountdown() {
time -= 1
displayTime.text = String(time)
if time == 0 {
timer.invalidate()
}
}

Fade out AVAudioEngine AVAudioPlayerNode

I´m trying to fade out an mp3 file using AVAudioEngine and AVAudioPlayerNode. I cannot figure it out. Can anybody please help in Swift?
I tried some things already. Everything works quite good but not as smooth as I would like it to be.
var fadeOutPlayer: Float = playerVolume
var fadeOutQuinte: Float = playerQuinteVolume
while fadeOutPlayer > 0.0 {
player.volume = fadeOutPlayer
playerQuinte.volume = fadeOutQuinte
fadeOutPlayer -= 0.001
fadeOutQuinte -= 0.001
usleep(100)
}
I also tried a timer (which doesn't do a fade):
if !isTimerFadeOutRunning {
player.volume = 0
playerQuinte.volume = 0
counter = 0
timerFadeOut = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(runTimerFadeOut), userInfo: nil, repeats: true)
isTimerFadeOutRunning = true
}
}
#objc func runTimerFadeOut() {
counter += 0.1
if counter == 0.2 {
player.pause()
playerQuinte.pause()
}
}
I've attached an AVAudioUnitEQ to my flow before mainMixerNode and then controlled the globalGain. The following code shows the idea:
private func manageFadeOut(volume: Double, fadeOutSecs: Double) {
let timeSecs = 0.001
DispatchQueue.main.asyncAfter(deadline: .now() + timeSecs) {
if !self.isFadingOut { return }
for fx in self.fadeOutEffects { fx.globalGain = Float(-96 * (1.0 - volume)) }
if volume <= 0 {
for player in self.players { player.stop() }
self.isFadingOut = false
return
}
let fade = timeSecs / fadeOutSecs
self.manageFadeOut(volume: volume - fade, fadeOutSecs: fadeOutSecs)
}
}

How to animate and move player node simultaneously, using sprite kit scene?

I'm working on a small ios game with this joystick library implemented onto it. My issue is after calculating the direction the joystick is going, I want the character to change to a running animation (I implement the animation using an .sks file). It almost works except after the animation has begun, it stops and doesn't finish until the player lets go at the stick. Some of my code is down below. Any help is appreciated.
Function to setup stick:
func setupJoystick() {
addChild(analogJoyStick)
analogJoyStick.trackingHandler = { [unowned self] data in
self.thePlayer.position = CGPoint(x: self.thePlayer.position.x + (data.velocity.x * 0.04), y: self.thePlayer.position.y + (data.velocity.y * 0.04))
let degrees = self.analogJoyStick.data.angular * 360 / (2 * .pi)
if degrees > 0 {
let walkAnimation:SKAction = SKAction(named: "WalkLeft")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
} else if degrees < 0 {
let walkAnimation:SKAction = SKAction(named: "WalkRight")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
}
analogJoyStick.beginHandler = { [unowned self] in
let degrees = self.analogJoyStick.data.angular * 360 / (2 * .pi)
if degrees > 0 {
let walkAnimation:SKAction = SKAction(named: "WalkLeft")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
} else if degrees < 0 {
let walkAnimation:SKAction = SKAction(named: "WalkRight")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
}
analogJoyStick.stopHandler = { [unowned self] in
self.thePlayer.removeAction(forKey: "animating")
}
}
Here is a visual of the coding:
Spritekit Demo
I read the joystick library instructions and saw two methods (handlers) you can use:
var beginHandler: (() -> Void)? // before move
var stopHandler: (() -> Void)? // after move
In beginHandler() add the (repeated) walk animation:
let walkAnimation: SKAction = SKAction(named: theAnimation)!
thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
And remove the action in stopHandler()
thePlayer.removeAction(forKey: "animating")
Or using the closures (also from the documentation):
joystick.beginHandler = { [unowned self] in
let walkAnimation: SKAction = SKAction(named: ".sks")!
self.thePlayer.run(SKAction.repeatForever(walkAnimation), withKey: "animating")
}
joystick.stopHandler = { [unowned self] in
self.thePlayer.removeAction(forKey: "animating")
}

Swift Countdown Function

I am attempting to create a countdown timer for a game using SpriteKit, but whenever I try to run countDown(), my game freezes. I am pretty sure my logic is correct here. I do not know what is going on.
func countDown(){
let countDownWait = SKAction.wait(forDuration: 1.0)
repeat {
self.run(countDownWait){
self.countDownTime -= 1
}
} while (self.countDownTime > 0)
if self.countDownTime == 0{
self.runGameOver()
}
}
you can do some checking in the update func for time passed or use a SKAction to track time similar to what you were doing in your code
let someLabel = SKLabelNode()
func countdown() {
var offset: Double = 0
for x in (0...10).reversed() {
run(SKAction.wait(forDuration: offset)) {
someLabel.text = "\(x)"
if x == 0 {
//do something when counter hits 0
//self.runGameOver()
}
else {
//maybe play some sound tick file here
}
}
offset += 1.0
}
}
Here's how I solved this problem for my Swift/SpriteKit 'Breakout' application. I wanted a countdown from 5 to 1 onthe main game screen but before the ball started to move. I Added these functions and then a call to countdown(5) at the end of didMoveToView: Notice the ball.physicsBody!.applyImpulse(CGVectorMake(20, 20)) as the last step of endCountdownwhich starts the ball and effectively starts the game.
func countdown(count: Int) {
countdownLabel.horizontalAlignmentMode = .Center
countdownLabel.verticalAlignmentMode = .Baseline
countdownLabel.position = CGPoint(x: size.width/2, y: size.height*(1/3))
countdownLabel.fontColor = SKColor.whiteColor()
countdownLabel.fontSize = size.height / 30
countdownLabel.zPosition = 100
countdownLabel.text = "Launching ball in \(count)..."
addChild(countdownLabel)
let counterDecrement = SKAction.sequence([SKAction.waitForDuration(1.0),
SKAction.runBlock(countdownAction)])
runAction(SKAction.sequence([SKAction.repeatAction(counterDecrement, count: 5),
SKAction.runBlock(endCountdown)]))
}
func countdownAction() {
count--
countdownLabel.text = "Launching ball in \(count)..."
}
func endCountdown() {
countdownLabel.removeFromParent()
ball.physicsBody!.applyImpulse(CGVectorMake(20, 20))
}
Try this solution to run countdown...
var seconds = 7200
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
runTimer()
}
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(self.updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
if seconds < 1 {
timer.invalidate()
//Send alert to indicate time's up.
} else {
seconds -= 1
timerLabel.text = timeString(time: TimeInterval(seconds))
}
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
Have you considered using a timer? Here is a good tutorial. It might take you 20 minutes to go through it or so, but it goes into detail about starting, stopping, pausing, and more for timers.