Audio playback progress as UISlider in Swift

29,882

Solution 1

Thanks to the suggestion of Dare above, here's how I accomplished this:

var updater : CADisplayLink! = nil

@IBAction func playAudio(sender: AnyObject) {
    playButton.selected = !(playButton.selected)
    if playButton.selected {
        updater = CADisplayLink(target: self, selector: Selector("trackAudio"))
        updater.frameInterval = 1
        updater.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
        let fileURL = NSURL(string: toPass)
        player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
        player.numberOfLoops = -1 // play indefinitely
        player.prepareToPlay()
        player.delegate = self
        player.play()
        startTime.text = "\(player.currentTime)"
        theProgressBar.minimumValue = 0
        theProgressBar.maximumValue = 100 // Percentage
    } else {
        player.stop()
    }
}

func trackAudio() {
    var normalizedTime = Float(player.currentTime * 100.0 / player.duration)
    theProgressBar.value = normalizedTime
}

@IBAction func cancelClicked(sender: AnyObject) {
    player.stop()
    updater.invalidate()
    dismissViewControllerAnimated(true, completion: nil)

}

Solution 2

Just to elaborate on my previous comment, this is how I implemented it and it seems to work pretty well. Any Swift corrections are more than welcome, I'm still an Obj-C guy for now.

@IBAction func playAudio(sender: AnyObject) {

    var playing = false

    if let currentPlayer = player {
        playing = player.playing;
    }else{
        return;
    }

    if !playing {
        let filePath = NSBundle.mainBundle().pathForResource("3e6129f2-8d6d-4cf4-a5ec-1b51b6c8e18b", ofType: "wav")
        if let path = filePath{
            let fileURL = NSURL(string: path)
            player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
            player.numberOfLoops = -1 // play indefinitely
            player.prepareToPlay()
            player.delegate = self
            player.play()

            displayLink = CADisplayLink(target: self, selector: ("updateSliderProgress"))
            displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode!)
        }

    } else {
        player.stop()
        displayLink.invalidate()
    }
}

func updateSliderProgress(){
    var progress = player.currentTime / player.duration
    timeSlider.setValue(Float(progress), animated: false)
}

*I set time slider's range between 0 and 1 on a storyboard

Solution 3

Syntax has now changed in Swift 4:

updater = CADisplayLink(target: self, selector: #selector(self.trackAudio))
updater.preferredFramesPerSecond = 1
updater.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)

And the function (I have previously set the progressSlider.maxValue to player.duration):

@objc func trackAudio() {
    progressSlider.value = Float(player!.currentTime)
 }

Solution 4

Specifically for Swift I was able to handle it like this:

  1. I set the maximum value of the scrubSlider to the duration of the music file(.mp3) that was loaded in this method

    override func viewDidLoad() {
        super.viewDidLoad()
    
        do {
    
            try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("bach", ofType: "mp3")!))
    
            scrubSlider.maximumValue = Float(player.duration)
    
        } catch {
            //Error
        } 
    
        _ = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ViewController.updateScrubSlider), userInfo: nil, repeats: true)
    
    }
    
  2. I player was set to play the music at the time set by the value of the scrubber.

    @IBAction func scrub(sender: AnyObject) {
    
        player.currentTime = NSTimeInterval(scrubSlider.value)
    
    }
    
Share:
29,882

Related videos on Youtube

allocate
Author by

allocate

VP of Engineering @ Penn Interactive

Updated on February 17, 2020

Comments

  • allocate
    allocate about 4 years

    I've seen some posts about accomplishing this in Objective-C but I've been unable to do the same via Swift.

    Specifically, I can't figure out how to implement addPeriodicTimeObserverForInterval in the below.

    var player : AVAudioPlayer! = nil
    
    @IBAction func playAudio(sender: AnyObject) {
        playButton.selected = !(playButton.selected)
        if playButton.selected {
            let fileURL = NSURL(string: toPass)
            player = AVAudioPlayer(contentsOfURL: fileURL, error: nil)
            player.numberOfLoops = -1 // play indefinitely
            player.prepareToPlay()
            player.delegate = self
            player.play()
            startTime.text = "\(player.currentTime)"
            endTime.text = NSString(format: "%.1f", player.duration)
        } else {
            player.stop()
        }
    

    Any assistance would be appreciated.

    • Dare
      Dare about 9 years
      If you are trying to update the slider as audio progresses, I like to use a display link. CADisplayLink will sync up to the screen refresh rate and call a selector on refresh like a graphics optimized NSTimer. They're also super simple to use and prevent you from having to queue up UIUpdates if the timer fires faster than the screen redraws. These are the docs. You can set the selector the display link calls to check the progress and update your slider position.
    • allocate
      allocate about 9 years
      @Dare, thanks for that. I had no idea that existed.
  • Dare
    Dare about 9 years
    One quick note – Make sure you invalidate the display link when you're done with it. If you don't it's gonna fire ad infinitum regardless of the audio player's state until something crashes.
  • allocate
    allocate about 9 years
    @Dare thanks for that. I'd incorrectly assumed dismissing the ViewController would terminate that (and I was very wrong). Code updated with the global declaration and the invalidate() call.
  • ChuckZHB
    ChuckZHB over 4 years
    That is what I need, in my case there is an additional audio playback timer under the slider bar. And touching the slider to pause the updaterupdater.isPaused = true and to resume it when user releases dragging from the slider bar.
  • ChuckZHB
    ChuckZHB over 4 years
    How does your updater worked in background mode? I just find that my slider cannot resume to process when re-open it from locked screen command center.