UISlider to control AVAudioPlayer

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


Solution 1:

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

Solution 2:

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.

Solution 3:

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);

Solution 4:

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