共计 5433 个字符,预计需要花费 14 分钟才能阅读完成。
播放很简单
一般分为两个过程,准备播放,与播放
准备播放,包括准备播放资源、播放器初始化和播放器准备好
其中准备播放资源
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()
, 一行代码
第一点,进度条怎么做?
一般进度条,会做两件事,
随着播放的推移,进度条的滑块会一直向前走,有一个音乐播放与进度条的进展的匹配
进度条的滑块可以拖拽,来控制当前播放的地方,譬如可以回播,可以跳过
播放音乐,进度条的滑块也走,进度是匹配的
每次播放前,先设置进度条的进度,
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
第二点,乱序播放与循环播放,怎么做?
使用乱序播放与循环播放两个按钮,针对的都是下一曲,以及之后的曲子
他们不会影响当前的歌曲播放
所以这两个按钮点击,都是改 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 保活就可以了
do {
//keep alive audio at background
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
} catch _ { }
do {try AVAudioSession.sharedInstance().setActive(true)
} catch _ {}
第四点,播放器的远程控件事件
屏幕的底部弹框中,播放器的控件事件
锁屏后,播放器的控件事件
首先要接收远程的控件事件
//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
本文代码:https://github.com/coyingcat/…
本文基于 bpolat/Music-Player