Search code examples
swiftavfoundationavplayer

AVPlayer fadeIn


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

Solution

  • 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()
        }
    }