UISlider to control AVAudioPlayer
Asked Answered
D

7

34

I'm trying to implement a little function in my app. I am currently playing sounds as AVAudioPlayers and that works fine. What I would like to add is to control the sound's position (currentTime) with an UISlider: is there a simple way to do it ?

I looked at an Apple project but it was quite messy....have you got samples or suggestions ?

Thanks to everyone in advance

Digastric answered 16/4, 2010 at 17:6 Comment(1)
This answer https://mcmap.net/q/451107/-how-to-make-the-progressbar-uislider-work-dynamically-in-my-app-duplicate shows much better way of tracking time playedAntonina
C
16

To extend on paull's answer, you'd set the slider to be continuous with a maximum value of your audio player's duration, then add some object of yours (probably the view controller) as a target for the slider's UIControlEventValueChanged event; when you receive the action message, you'd then set the AVAudioPlayer's currentTime property to the slider's value. You might also want to use an NSTimer to update the slider's value as the audio player plays; +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: is the easiest way to do that.

Contagion answered 16/4, 2010 at 17:25 Comment(2)
I'm really new to iphone development, so I gently ask you to follow me while doing everything....so in the viewDidLoad method I start the audio, and added this: _progressBar.continuous = YES; _progressBar.minimumValue = 0.0; _progressBar.maximumValue = suono01.duration; [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES]; Now I what do I need to do use UIControlEventValueChanged ?Digastric
I continued like this but if I move the slider, currentTime doesn't move: - (void)onTimer{ UISlider *sliderozzo = _progressBar; [sliderozzo addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventValueChanged]; _progressBar.value = [suono01 currentTime]; } - (void)buttonClicked{ _progressBar.value = [suono01 currentTime]; }Digastric
D
52

Shouldn't be a problem - just set the slider to continuous and set the max value to your player's duration after loading your sound file.

Edit

I just did this and it works for me...

- (IBAction)slide {
    player.currentTime = slider.value;
}

- (void)updateTime:(NSTimer *)timer {
    slider.value = player.currentTime;
}

- (IBAction)play:(id)sender {
    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"sound.caf" ofType:nil]];
    NSError *error;
    player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    if (!player) NSLog(@"Error: %@", error);
    [player prepareToPlay];
    slider.maximumValue = [player duration];
    slider.value = 0.0;
    
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTime:) userInfo:nil repeats:YES];  
    [player play];
}

The slider is configured in IB, as is a button to start playing.

Swift 3.0 Update:

var player: AVAudioPlayer!
var sliderr: UISlider!

@IBAction func play(_ sender: Any) {
    var url = URL(fileURLWithPath: Bundle.main.path(forResource: "sound.caf", ofType: nil)!)
    var error: Error?
    do {
        player = try AVAudioPlayer(contentsOf: url)
    }
    catch let error {
    }
    if player == nil {
        print("Error: \(error)")
    }
    player.prepareToPlay()
    sliderr.maximumValue = Float(player.duration)
    sliderr.value = 0.0
    Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
    player.play()
}

func updateTime(_ timer: Timer) {
    sliderr.value = Float(player.currentTime)
}

@IBAction func slide(_ slider: UISlider) {
    player.currentTime = TimeInterval(slider.value)
}
Deliberate answered 16/4, 2010 at 17:20 Comment(3)
I coded like this, it builds correctly but it doesn't work: _progressBar.continuous = YES; _progressBar.value = [suono01 currentTime]; _progressBar.minimumValue = 0.0; _progressBar.maximumValue = suono01.duration;Digastric
thanks man..this works perfectly good..i ve got a problem ...if i m to slide my seek bar its getting crashedHepsibah
Thanks @Paul, Please provide code for seek the audio when manually slider value changed.Weisburgh
C
16

To extend on paull's answer, you'd set the slider to be continuous with a maximum value of your audio player's duration, then add some object of yours (probably the view controller) as a target for the slider's UIControlEventValueChanged event; when you receive the action message, you'd then set the AVAudioPlayer's currentTime property to the slider's value. You might also want to use an NSTimer to update the slider's value as the audio player plays; +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: is the easiest way to do that.

Contagion answered 16/4, 2010 at 17:25 Comment(2)
I'm really new to iphone development, so I gently ask you to follow me while doing everything....so in the viewDidLoad method I start the audio, and added this: _progressBar.continuous = YES; _progressBar.minimumValue = 0.0; _progressBar.maximumValue = suono01.duration; [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(onTimer) userInfo:nil repeats:YES]; Now I what do I need to do use UIControlEventValueChanged ?Digastric
I continued like this but if I move the slider, currentTime doesn't move: - (void)onTimer{ UISlider *sliderozzo = _progressBar; [sliderozzo addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventValueChanged]; _progressBar.value = [suono01 currentTime]; } - (void)buttonClicked{ _progressBar.value = [suono01 currentTime]; }Digastric
D
8

I needed to adapt the above answer a bit to get it to work. The issue is that using

slider.maximumValue = [player duration];
slider.value = player.currentTime;
player.currentTime = slider.value;

Do not work because the slider expects a float and the player currentTime and dration return CMTime. To make these work, I adapted them to read:

slider.maximumValue = CMTimeGetSeconds([player duration]);
slider.value = CMTimeGetSeconds(player.currentTime);
player.currentTime = CMTimeMakeWithSeconds((int)slider.value,1);
Dreadnought answered 10/11, 2012 at 19:8 Comment(1)
Indeed it works. I don't see a need to convert this values to seconds cos they are already in seconds...Sciatica
W
1

If you don't need any data in between drag, then you should simply set: mySlider.isContinuous = false

Otherwise, try below code to controller each phase of touch.

// audio slider bar
    private lazy var slider: UISlider = {
        let slider = UISlider()
        slider.translatesAutoresizingMaskIntoConstraints = false
        slider.minimumTrackTintColor = .red
        slider.maximumTrackTintColor = .white
        slider.setThumbImage(UIImage(named: "sliderThumb"), for: .normal)
        
        slider.addTarget(self, action: #selector(onSliderValChanged(slider:event:)), for: .valueChanged)
//        slider.isContinuous = false
        
        return slider
    }()
    
    @objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
        guard let player = AudioPlayer.shared.player else { return }
        if let touchEvent = event.allTouches?.first {
            switch touchEvent.phase {
            case .began:
                // handle drag began
                // I would stop the timer when drag begin
                timer.invalidate()
            case .moved:
                // handle drag moved
                // Update label's text for current playing time
            case .ended:
                // update the player's currTime and re-create the timer when drag is done.
                player.currentTime = TimeInterval(slider.value)
                timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTime(_:)), userInfo: nil, repeats: true)
            default:
                break
            }
        }
    }

Wynellwynn answered 3/6, 2021 at 11:38 Comment(0)
A
1

Here's the entire setup for an AVAudioPlayer. Some of the code in handleScrubbing() and fingerLiftedFromSlider() is duplicated but whatever...

This will let you show what's on the currentTimeLabel (usually on the left) and the totalDurationLabel (usually on the right) with the scrubber/slider in the the middle of them. When you slide the slider the currentTime will update to show wherever the slider is.

There is something to be aware about. If the the player was playing before you touch the slider, while you slide the slider, the player is still playing. In .began you need to check if the player was playing and if so pause it and set a variable like wasAudioPlayerPlayingBeforeSliderWasTouched to true so that when your finger is lifted it will continue playing from wherever you lift your finger. If you don't pause the player then the slider isn't going to slide smoothly.

When you lift your finger there is a check in onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() to see if the slider is at its endTime. If it is instead of playing it'll run the code in audioEndTimeStopEverything().

In the startAudioPlayer method, I used an AVURLAsset get a set the actual url's duration. I got it from this answer which has a great explanation.

I used this code with a local url, not sure how this will work with a remote url.

import UIKit
import AVFoundation

class MyAudioController: UIViewController {

    lazy var currentTimeLabel ... { ... }()
    lazy var totalDurationLabel ... { ... }()
    lazy vay pausePlayButton ... { ... }()
    lazy var fastForwardButton ... { ... }()
    lazy var rewindButton ... { ... }()

    lazy var slider: UISlider = {
        let s = UISlider()
        s.translatesAutoresizingMaskIntoConstraints = false
        
        s.isContinuous = true
        s.minimumTrackTintColor = UIColor.red
        s.maximumTrackTintColor = UIColor.blue

        s.setThumbImage(UIImage(named: "circleIcon"), for: .normal)
        s.addTarget(self, action: #selector(sliderValChanged(slider:event:)), for: .valueChanged)
        return s
    }()

    weak var timer: Timer? // *** MAKE SURE THIS IS WEAK ***
    var audioPlayer: AVAudioPlayer?
    var wasAudioPlayerPlayingBeforeSliderWasTouched = false

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let myAudioUrl = URL(string: "...") else { return }

        setAudio(with: myAudioUrl)

        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print(error)
        }
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        stopAudioPlayer()
    }

    // 1. init your AVAudioPlayer here
    func setAudioPlayer(with audioTrack: URL) {
        
        do {
            
            stopAudioPlayer() // if something was previously playing

            audioPlayer = try AVAudioPlayer(contentsOf: audioTrack)
            
            audioPlayer?.delegate = self

            audioPlayer?.prepareToPlay()
            
            audioPlayer?.volume = audioVolume
            
            startAudioPlayer()
            
        } catch let err as NSError {
            
            print(err.localizedDescription)
        }
    }

    // 2. Audio PLAYER  - start / stop funcs
    stopAudioPlayer() {
        
        stopAudioTimer()
        
        audioPlayer?.pause()
        audioPlayer?.stop()
    }

    func startAudioPlayer() {
        
        if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
            audioPlayer.pause()
        }

        audioPlayer?.currentTime = 0
        
        audioPlayer?.play()
        pausePlayButton.setImage(UIImage(named: "pauseIcon"), for: .normal)

        startAudioTimer()
    }

    func startAudioTimer() {

        stopAudioTimer()
        slider.value = 0
        currentTimeLabel.text = "00:00"
        totalDurationLabel.text = "00:00"
        
        guard let url = audioPlayer?.url else { return }

        let assetOpts = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
        let asset = AVURLAsset(url: url, options: assetOpts)
        let assetDuration: CMTime = asset.duration
        let assetDurationInSecs: Float64 = CMTimeGetSeconds(assetDuration)

        slider.maximumValue = Float(assetDurationInSecs)

        totalDurationLabel.text = strFromTimeInterval(interval: TimeInterval(assetDurationInSecs))
        
        runAudioTimer()
    }

    // 3. TIMER funcs
    func runAudioTimer() {
        
        if timer ==  nil {
            
            timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { [weak self](_) in
                self?.audioTimerIsRunning()
            })
        }
    }
    
    func audioTimerIsRunning() {
        guard let audioPlayer = audioPlayer else { return }
        
        let currentTime = audioPlayer.currentTime

        if Float(currentTime) >= Float(slider.maximumValue) {
            stopAudioTimer()
        }

        currentTimeLabel.text = strFromTimeInterval(interval: currentTime)
        
        slider.value = Float(currentTime)
    }

    func stopAudioTimer() {
        
        if timer != nil {
            
            timer?.invalidate()
            timer = nil
        }
    }

    // slider funcs
    @objc func sliderValChanged(slider: UISlider, event: UIEvent) {
        if let touchEvent = event.allTouches?.first {
            switch touchEvent.phase {
            case .began:
                checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched()
                stopAudioTimer()
                print("Finger Touched")
            case .moved:
                handleScrubbing()
                print("Finger is Moving Scrubber")
            case .ended:
                print("Finger Lifted")
                onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan()
                fingerLiftedFromSlider()
            default:
                print("Something Else Happened In Slider")
            }
        }
    }

    func checkIfAudioPlayerWasPlayingWhenSliderIsFirstTouched() {
        guard let audioPlayer = audioPlayer else { return }

        if audioPlayer.isPlaying {
            audioPlayer.pause()
            wasAudioPlayerPlayingBeforeSliderWasTouched = true
        }
    }

    func handleScrubbing() {
        guard let audioPlayer = audioPlayer else { return }
        
        let sliderValue = TimeInterval(slider.value)
        
        currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)

        audioPlayer.currentTime = sliderValue

        if audioPlayer.currentTime >= audioPlayer.duration {
            audioEndTimeStopEverything()
        }
    }

    func onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan() {

        if wasAudioPlayerPlayingBeforeSliderWasTouched {
            wasAudioPlayerPlayingBeforeSliderWasTouched = false

            guard let audioPlayer = audioPlayer else { return }

            if slider.value >= slider.maximumValue {
                audioEndTimeStopEverything()
            } else {
                audioPlayer.play()
            }
        }
    }

    func fingerLiftedFromSlider() {
        guard let audioPlayer = audioPlayer else { return }
        
        if !audioPlayer.isPlaying { // this check is necessary because if you paused the audioPlayer, then started sliding, it should still be paused when you lift you finger up. It it's paused there is no need for the timer function to run.
            
            let sliderValue = TimeInterval(slider.value)
        
            currentTimeLabel.text = strFromTimeInterval(interval: sliderValue)

            audioPlayer.currentTime = sliderValue
            
            return
        }
        
        runAudioTimer()
    }

    func audioEndTimeStopEverything() {
        
        stopAudioPlayer()
        pausePlayButton.setImage(UIImage("named: playIcon"), for: .normal)

        guard let audioPlayer = audioPlayer else { return }

        // for some reason when the audioPlayer would reach its end time it kept resetting its currentTime property to zero. I don't know if that's meant to happen or a bug but the currentTime would be zero and the slider would be at the end. To rectify the issue I set them both to their end times
        audioPlayer.currentTime = audioPlayer.duration
        slider.value = slider.maximumValue
    }
}

extension MyAudioController: AVAudioPlayerDelegate {
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        
        audioEndTimeStopEverything()
    }
    
    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
        if let error = error {
            print(error.localizedDescription)
        }
    }
}

Here is the strFromTimeInterval(interval: ) function that I got from here. I only used it because I didn't want to bother with milliseconds. The code above was ran using audio files with minutes and seconds, not hours. If you have any problems with hours you can also swap this function out for this answer

extension MyAudioController {

    func strFromTimeInterval(interval: TimeInterval) -> String {
        
        let time = NSInteger(interval)
        
        let seconds = time % 60
        let minutes = (time / 60) % 60
        let hours = (time / 3600)
        
        var formatString = ""
        if hours == 0 {
            if (minutes < 10) {
                
                formatString = "%2d:%0.2d"
            } else {
                
                formatString = "%0.2d:%0.2d"
            }
            return String(format: formatString,minutes,seconds)
        } else {
            
            formatString = "%2d:%0.2d:%0.2d"
            return String(format: formatString,hours,minutes,seconds)
        }
    }
}
Alcaic answered 26/11, 2021 at 4:43 Comment(0)
G
0

Problems that I've faced during playing an audio file and show start/end time and controlling the song with the UISlider.

  1. Not playing audio directly without downloading it in temp folder.
  2. UISlider got crashed on main thread in lower iOS version i.e 12.4/13.1
  3. Smooth Scrolling of UISlider.
  4. Calculating and updating the start/end time of the song.

This answer needs some editing, but it will work without any doubt.

  //UISlider init
  lazy var slider: UISlider = {
    let progress = UISlider()
    progress.minimumValue = 0.0
    progress.maximumValue = 100.0
    progress.tintColor = UIColor.init(named: "ApplicationColor")
    return progress

}()

 var audioPlayer : AVAudioPlayer?
   //First I've downloaded the audio and then playing it.
  override func viewWillAppear(_ animated: Bool) {

    super.viewWillAppear(animated)


    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(trackAudio), userInfo: nil, repeats: true)

    if let audioURLString = audioURL{
        let urlstring = URL(string: audioURLString)!
        downloadFromURL(url: urlstring) { (localURL, response, error) in
            if let localURL = localURL{
                self.playAudioFile(url: localURL)
            }

        }
    }
}

 override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    stopTimer()
}


// Stop TimeInterval After View disappear
func stopTimer() {
    if timer != nil {
        timer?.invalidate()
        audioPlayer?.stop()
        audioPlayer = nil
        timer = nil
    }
}
@objc func sliderSelected(_ sender : UISlider){

    if audioPlayer != nil{
        if !isPlaying{
            self.audioPlayer?.play()
            playButton.setImage(UIImage.init(named: "AudioPause"), for: .normal)
            isPlaying = true
        }else{
            self.audioPlayer?.currentTime = TimeInterval(Float(sender.value) * Float(self.audioPlayer!.duration) / 100.0)
            if (sender.value / 100.0 == 1.0){

                //Do something if audio ends while dragging the UISlider.

            }
        }


    }

}
 func downloadFromURL(url:URL,completion: @escaping((_ downladedURL: URL?,_ response :URLResponse?,_ error: Error?) -> Void)){
    var downloadTask:URLSessionDownloadTask
    downloadTask = URLSession.shared.downloadTask(with: url) {(URL, response, error) in
        if let url = URL{
            completion(url,nil,nil)
        }else if let response = response{
            completion(nil,response,nil)
        }
        if let error = error{
            completion(nil,nil,error)
        }


    }

    downloadTask.resume()
}


func playAudioFile(url:URL){
    do{

        self.audioPlayer = try AVAudioPlayer(contentsOf: url)
        self.audioPlayer?.prepareToPlay()
        self.audioPlayer?.delegate = self
        self.audioPlayer?.play()

   let audioDuration = audioPlayer?.duration
        let audioDurationSeconds = audioDuration
        minutes = Int(audioDurationSeconds!/60);
        seconds =  Int(audioDurationSeconds!.truncatingRemainder(dividingBy: 60))
    } catch{
        print("AVAudioPlayer init failed")
    }
}

@objc func trackAudio() {

    if audioPlayer != nil{
        DispatchQueue.main.async {
            print("HI")
            let normalizedTime = Float(self.audioPlayer!.currentTime * 100.0 / self.audioPlayer!.duration)
            self.slider.setValue(normalizedTime, animated: true)
            let currentTime = self.audioPlayer?.currentTime
            self.currentMinutes = Int(currentTime!/60);
            self.currentSeconds =  Int(currentTime!.truncatingRemainder(dividingBy: 60))
            self.startTimeLabel.text =  String(format: "%02i:%02i", self.currentMinutes, self.currentSeconds)
            self.endTimeLabel.text = String(format: "%02i:%02i", self.minutes, self.seconds)
        }
    }





}
Glendoraglendower answered 13/2, 2020 at 12:23 Comment(0)
F
0

If anyone was looking for a simple TouchDown and TouchUp on UI slider then this turns out to be as simple as :

    slider.addTarget(self, action: #selector(changeVlaue(_:)), for: .valueChanged)
    slider.addTarget(self, action: #selector(sliderTapped), for: .touchDown)
    slider.addTarget(self, action: #selector(sliderUntouched), for: .touchUpInside)
Fried answered 8/5, 2021 at 14:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.