21

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.

allocate
  • 1,323
  • 3
  • 14
  • 28
  • 5
    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.](https://developer.apple.com/library/prerelease/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/index.html) You can set the selector the display link calls to check the progress and update your slider position. – Dare Apr 09 '15 at 15:08
  • @Dare, thanks for that. I had no idea that existed. – allocate Apr 09 '15 at 16:33

4 Answers4

33

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)

}
allocate
  • 1,323
  • 3
  • 14
  • 28
  • 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. – Dare Apr 09 '15 at 18:58
  • @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. – allocate Apr 09 '15 at 19:13
  • 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 updater```updater.isPaused = true``` and to resume it when user releases dragging from the slider bar. – Zhou Haibo Sep 30 '19 at 13:23
  • 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. – Zhou Haibo Oct 25 '19 at 03:16
9

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

Dare
  • 2,497
  • 1
  • 12
  • 20
5

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)
    
    }
    
kRiZ
  • 2,320
  • 4
  • 28
  • 39
Laban Bwire
  • 81
  • 2
  • 7
5

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)
 }
BlackVoid
  • 101
  • 1
  • 5