34

我正在尝试在我的应用程序中实现一个小功能。我目前正在以 AVAudioPlayers 播放声音,效果很好。我想补充的是用 UISlider 控制声音的位置(currentTime):有没有简单的方法来做到这一点?

我查看了一个 Apple 项目,但它非常混乱......你有样品或建议吗?

提前感谢大家

4

7 回答 7

52

不应该是一个问题 - 只需将滑块设置为连续并将最大值设置为加载声音文件后播放器的持续时间。

编辑

我刚做了这个,它对我有用......

- (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];
}

滑块在 IB 中配置,作为开始播放的按钮。

斯威夫特 3.0 更新:

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)
}
于 2010-04-16T17:20:29.100 回答
16

为了扩展保罗的回答,您将滑块设置为与音频播放器的最大值连续duration,然后添加您的一些对象(可能是视图控制器)作为滑块UIControlEventValueChanged事件的目标;当您收到操作消息时,您会将AVAudioPlayer'currentTime属性设置为滑块的值。您可能还想NSTimer在音频播放器播放时使用 更新滑块的值;+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:是最简单的方法。

于 2010-04-16T17:25:27.647 回答
8

我需要稍微调整一下上面的答案才能让它工作。问题是使用

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

不工作,因为滑块需要一个浮点数并且播放器 currentTime 和 dration 返回 CMTime。为了使这些工作,我将它们改编为:

slider.maximumValue = CMTimeGetSeconds([player duration]);
slider.value = CMTimeGetSeconds(player.currentTime);
player.currentTime = CMTimeMakeWithSeconds((int)slider.value,1);
于 2012-11-10T19:08:40.957 回答
1

如果您在拖动之间不需要任何数据,那么您应该简单地设置: mySlider.isContinuous = false

否则,请尝试以下代码来控制触摸的每个阶段。

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

于 2021-06-03T11:38:07.273 回答
0

我在播放音频文件和显示开始/结束时间以及使用 UISlider 控制歌曲时遇到的问题。

  1. 不直接播放音频而不将其下载到临时文件夹中。
  2. UISlider 在较低的 iOS 版本(即 12.4/13.1)的主线程上崩溃
  3. UISlider 的平滑滚动。
  4. 计算和更新歌曲的开始/结束时间。

这个答案需要一些编辑,但毫无疑问它会起作用。

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





}
于 2020-02-13T12:23:50.327 回答
0

如果有人在 UI 滑块上寻找一个简单的 TouchDown 和 TouchUp,那么这很简单:

    slider.addTarget(self, action: #selector(changeVlaue(_:)), for: .valueChanged)
    slider.addTarget(self, action: #selector(sliderTapped), for: .touchDown)
    slider.addTarget(self, action: #selector(sliderUntouched), for: .touchUpInside)
于 2021-05-08T14:15:49.937 回答
0

这是AVAudioPlayer. handleScrubbing()fingerLiftedFromSlider()中的一些代码是重复的,但无论如何......

这将让您显示 currentTimeLabel (通常在左侧)和 totalDurationLabel (通常在右侧)上的内容,中间有洗涤器/滑块。当您滑动滑块时,currentTime 将更新以显示滑块所在的位置。

有一些事情需要注意。如果在您触摸滑块之前播放器正在播放,当您滑动滑块时,播放器仍在播放。.began您需要检查播放器是否正在播放,如果是则暂停播放,并将诸如 wasAudioPlayerPlayingBeforeSliderWasTouched 之类的变量设置为true这样当您抬起手指时,它会从您抬起手指的任何位置继续播放。如果您不暂停播放器,则滑块不会平滑滑动。

当您抬起手指时,会检查onFingerLiftedStartAudioPlayerIfItWasPlayingBeforeTouchesBegan()滑块是否处于结束时间。如果不是播放,它将运行audioEndTimeStopEverything().

startAudioPlayer方法中,我使用了AVURLAsset一个设置实际 url 的持续时间。我从这个答案中得到了它,它有一个很好的解释。

我将此代码与本地 url 一起使用,不确定这将如何与远程 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)
        }
    }
}

这是strFromTimeInterval(interval: )我从这里得到的功能。我只使用它是因为我不想打扰毫秒。上面的代码是使用带有分钟和秒的音频文件运行的,而不是小时。如果您对工作时间有任何问题,您也可以将此功能换成此答案

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)
        }
    }
}
于 2021-11-26T04:43:01.447 回答