开发一款 iOS 音乐播放器的五个点

4,920 阅读5分钟

播放很简单

一般分为两个过程,准备播放,与播放

准备播放,包括准备播放资源、播放器初始化和播放器准备好

其中准备播放资源


        var currentAudioPath:URL!

        currentAudio = readSongNameFromPlist(currentAudioIndex)
        if let path = Bundle.main.path(forResource: currentAudio, ofType: "mp3"){
            currentAudioPath = URL(fileURLWithPath: path)
        }
        else{
            alertSongExsit()
        }

播放器初始化和播放器准备好

        var audioPlayer:AVAudioPlayer!
     
       audioPlayer = try? AVAudioPlayer(contentsOf: currentAudioPath)
        audioPlayer.delegate = self
        
        audioLength = audioPlayer.duration
        playerProgressSlider.maximumValue = CFloat(audioPlayer.duration)
        playerProgressSlider.minimumValue = 0.0
        playerProgressSlider.value = 0.0
        
        
        audioPlayer.prepareToPlay()

播放

audioPlayer.play(), 一行代码

第一点,进度条怎么做?

111

一般进度条,会做两件事,

随着播放的推移,进度条的滑块会一直向前走,有一个音乐播放与进度条的进展的匹配

进度条的滑块可以拖拽,来控制当前播放的地方,譬如可以回播,可以跳过

播放音乐,进度条的滑块也走,进度是匹配的

每次播放前,先设置进度条的进度,

maximumValue 最大值,就是放完了,一首歌的时长

minimumValue 最小值,就是没播放,为 0

value 开始的时候,就是没播放,为 0

        playerProgressSlider.maximumValue = CFloat(audioPlayer.duration)
        playerProgressSlider.minimumValue = 0.0
        playerProgressSlider.value = 0.0

要想进度条的滑块会一直向前走,就要有一个计时器

func startTimer(){
        if timer == nil {
            timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PlayerViewController.update(_:)), userInfo: nil,repeats: true)
            timer.fire()
        }
    }
    
   // 每隔一秒,去获取播放器的当前播放时间,刷新进度条 playerProgressSlider 的状态
    @objc func update(_ timer: Timer){
        if !audioPlayer.isPlaying{
            return
        }
        let time = calculateTimeFromNSTimeInterval(audioPlayer.currentTime)
        playerProgressSlider.value = CFloat(audioPlayer.currentTime)
    }

拖拽进度条的滑块,调整播放的位置

因为之前滚动条的范围与播放器的时长,已经匹配好了

所以设置播放器的当前时间 currentTime ,就可以了

先暂停,再设置播放器的 currentTime,短暂的间隔后

过渡比较平滑,体验稍微好一些

@IBAction func changeAudioLocationSlider(_ sender : UISlider) {
        audioPlayer.pause()
        audioPlayer.currentTime = TimeInterval(sender.value)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.audioPlayer.play()
        } 
    }

做快进和快退,也是这个思路,更改播放器的当前时间 audioPlayer.currentTime

第二点,乱序播放与循环播放,怎么做?

222

使用乱序播放与循环播放两个按钮,针对的都是下一曲,以及之后的曲子

他们不会影响当前的歌曲播放

所以这两个按钮点击,都是改 UI , 改状态,当前歌曲播放完成后,起作用

或者当前没播放,下一次播放的时候,第一首歌曲播放完了,起作用

@IBAction func shuffleButtonTapped(_ sender: UIButton) {
        shuffleArray.removeAll()
        if sender.isSelected{
            sender.isSelected = false
            shuffleState = false
        } else {
            sender.isSelected = true
            shuffleState = true
        }
    }
    
    
    @IBAction func repeatButtonTapped(_ sender: UIButton) {
        if sender.isSelected == true {
            sender.isSelected = false
            repeatState = false
        } else {
            sender.isSelected = true
            repeatState = true
        }  
    }

乱序与循环,在 AVAudioPlayerDelegate 的播放完成回调方法中起作用 ,func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool){

单曲循环效果

就是指没点击乱序,只点击了循环,

       if shuffleState == false && repeatState == true {
                //repeat same song, 重复播放就可以了
                prepareAudio()
                playAudio()

乱序,不循环效果

就是没点击循环,只点击了乱序,

乱序,就要取随机数,

不循环,就要去除重复,这里就是把歌单播放一遍,就完了

// 通过建造一个数组来记录
   var shuffleArray = [Int]()

    if shuffleState == true && repeatState == false {
            // 放了一首,添加一个
            shuffleArray.append(currentAudioIndex)

              // 终止条件,放过的,不少于歌单的
              if shuffleArray.count >= audioList.count {
                  playButton.setImage( UIImage(named: "play"), for: UIControl.State())
                  return
                  
              }
              // 一个可优化的循环

              // 一直取随机数,如果取到没播放的,就添加下,跳出去,走下一步
              // 否则一直在这里算
              var randomIndex = 0
              var newIndex = false
              while newIndex == false {
                  randomIndex =  Int(arc4random_uniform(UInt32(audioList.count)))
                  if shuffleArray.contains(randomIndex) {
                      newIndex = false
                  }else{
                      newIndex = true
                  }
              }
              // 算出结果,赋值过去
              currentAudioIndex = randomIndex
              // 准备与播放
              prepareAudio()
              playAudio()

乱序循环效果

就是点击了循环和乱序

乱序,就要取随机数,

乱序循环,这里就是把歌单乱序播放一遍,再重来

// 通过建造一个数组来记录
        var shuffleArray = [Int]()

        if shuffleState == true && repeatState == true {
                //shuffle song endlessly
               
                 // 放了一首,添加一个
                shuffleArray.append(currentAudioIndex)

                // 重复条件,都播放过了,不少于歌单的,就清空重来
                if shuffleArray.count >= audioList.count {
                    shuffleArray.removeAll()
                }
                
                // 一个可优化的循环

                // 一直取随机数,如果取到没播放的,就添加下,跳出去,走下一步
                // 否则一直在这里算
                var randomIndex = 0
                var newIndex = false
                while newIndex == false {
                    randomIndex =  Int(arc4random_uniform(UInt32(audioList.count)))
                    if shuffleArray.contains(randomIndex) {
                        newIndex = false
                    }else{
                        newIndex = true
                    }
                }
                // 算出结果,赋值过去
                currentAudioIndex = randomIndex
                // 准备与播放
                prepareAudio()
                playAudio()

            }

第三点,锁屏播放与切换到其他应用播放

实际上就是后台播放

设置一下后台模式,让 session 保活就可以了

777

        do {
            //keep alive audio at background
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
        } catch _ { }
        
        do {
            try AVAudioSession.sharedInstance().setActive(true)
        } catch _ { }

第四点,播放器的远程控件事件

屏幕的底部弹框中,播放器的控件事件

333

锁屏后,播放器的控件事件

333

首先要接收远程的控件事件

        //LockScreen Media control registry
        if UIApplication.shared.responds(to: #selector(UIApplication.beginReceivingRemoteControlEvents)){
            UIApplication.shared.beginReceivingRemoteControlEvents()
            UIApplication.shared.beginBackgroundTask(expirationHandler: { () -> Void in
            })
        }

把播放信息,同步到锁屏播放器与底部弹窗的播放器

播放的时候,把播放信息,同步到锁屏与底部弹窗,

// This shows media info on lock screen - used currently and perform controls
    func showMediaInfo(){
        let artistName = readArtistNameFromPlist(currentAudioIndex)
        let songName = readSongNameFromPlist(currentAudioIndex)
        MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyArtist : artistName,  MPMediaItemPropertyTitle : songName]
    }

最后,重写 func remoteControlReceived 方法

锁屏的时候,可以对播放器暂停与播放,点击上一首,与下一首

拉起底部弹窗的时候,也是

override func remoteControlReceived(with event: UIEvent?) {
        if event!.type == UIEvent.EventType.remoteControl{
            switch event!.subtype{
            case UIEventSubtype.remoteControlPlay:
                play(self)
            case UIEventSubtype.remoteControlPause:
                play(self)
            case UIEventSubtype.remoteControlNextTrack:
                next(self)
            case UIEventSubtype.remoteControlPreviousTrack:
                previous(self)
            default:
                print("There is an issue with the control")
            }
        }
    }

第五点,怎么播放多种文件,mp3、m4a?

通过二进制的 data , 实例化 AVAudioPlayer 的方式

    var player: AVAudioPlayer!
     var tempPath: String?
        if let mpPath = Bundle.main.path(forResource: str, ofType: "mp3"){
            tempPath = mpPath
        }
        if let maPath = Bundle.main.path(forResource: str, ofType: "m4a"){
            tempPath = maPath
        }
       
        guard let path = tempPath, let playerTmp = try? AVAudioPlayer(data: Data(contentsOf: URL(fileURLWithPath: path))) else{
            return
        }
        self.player = playerTmp

本文代码: github.com/coyingcat/m…

本文基于 bpolat/Music-Player