Swift 3 :基于 AVAudioPlayer 的簡(jiǎn)單音樂(lè)播放器

學(xué)習(xí)ios以來(lái)差不多接近兩個(gè)月了绍填,作為一個(gè)剛?cè)胄械牟穗u終于鼓起勇氣寫博客并發(fā)布出來(lái),本周課程講到了ios多媒體應(yīng)用關(guān)于音頻播放這部分(本菜還在讀大學(xué)走的移動(dòng)端ios方向= =)盾饮,課下作業(yè)老師讓做個(gè)基于AVAudioPlayer的音樂(lè)播放器裳瘪,問(wèn)題不大细办,比較簡(jiǎn)單橙凳,想給自己加點(diǎn)難度蕾殴,最近swift這么火,干脆用swift寫個(gè)吧岛啸!于是逼著自己一點(diǎn)點(diǎn)的邊做邊啃swift語(yǔ)法钓觉,花了差不多3天的時(shí)間,粗制濫造 = =坚踩!的搞出來(lái)個(gè)簡(jiǎn)單版的音樂(lè)播放器(希望大家多多指點(diǎn)荡灾,供跟我一樣剛?cè)胄械呐笥鸦ハ嘟涣鳎ハ鄬W(xué)習(xí))瞬铸,如圖:


效果圖.gif

1.界面

最初想的是該怎么入門批幌,畢竟swift認(rèn)識(shí)我,我不認(rèn)識(shí)它吧そ凇荧缘!還是去世界上最大的同性交友網(wǎng)站GitHub上去看看有沒(méi)有關(guān)于swift播放器的demo把,從學(xué)習(xí)別人的代碼來(lái)入門拦宣,結(jié)果轉(zhuǎn)了一大圈都沒(méi)有找到合適的截粗,不過(guò)發(fā)現(xiàn)了個(gè)有四百個(gè)star的提供炫酷手勢(shì)界面殼子的項(xiàng)目,但僅僅是個(gè)殼子而已鸵隧,好吧我的界面就用它了绸罗,對(duì)了說(shuō)到界面,他界面用的是StoryBoard還用到了個(gè)新控件叫ContainerView豆瘫,ContainerView是用來(lái)在一個(gè)視圖控制器上添加子視圖控制器的珊蟀,他這樣做的好處就是可以讓迷你播放欄一只處于界面最上方,點(diǎn)擊tabbar切換界面的時(shí)候外驱,只需要將ContainerView里的子視圖控制器更換即可育灸。具體用法大家可以自行簡(jiǎn)書(shū)

2.獲取音樂(lè)及相關(guān)信息略步,封裝音樂(lè)播放類

界面有了描扯,使用AVAudioPlayer根據(jù)本地音樂(lè)的路徑進(jìn)行音樂(lè)播放定页,在這里我封裝了個(gè)方法用來(lái)獲取本地文件夾myMusic里所有歌曲路徑以及作家趟薄,封面,歌曲名等典徊。為了做出上一曲下一曲功能我將每首歌曲都編了號(hào)杭煎。

 static func getALL()->Array<Music>{
        if musicArry == nil {
            var musicArryList=Array<Music>()
            var fileArry:[String]?
            var num:Int=0;
            let path=Bundle.main.path(forResource:"mymusic", ofType: nil)
            do{
                try fileArry=FileManager.default.contentsOfDirectory(atPath: path!)
            }
            catch{
                print("error")
            }
            for n in fileArry! {
                let singlePath=path!+"/"+n
                let avURLAsset = AVURLAsset(url: URL.init(fileURLWithPath: singlePath))
                let musicModel:Music = Music()
                
                for i in avURLAsset.availableMetadataFormats {
                    
                    for j in avURLAsset.metadata(forFormat: i) {
                        //歌曲名
                        if j.commonKey == "title"{
                            musicModel.musicName = j.value as? String
                            
                        }//封面圖片
                        if j.commonKey == "artwork"{
                            musicModel.musicimg=j.value as? Data// 這里是個(gè)坑坑T T
                        }//專輯名
                        if j.commonKey == "albumName"{
                            musicModel.musicAlbum=j.value as? String
                        }
                        //歌手
                        if j.commonKey == "artist"{
                            musicModel.musicAuthor=j.value as? String
                        }
                    }
                    
                }
                musicModel.musicURL=URL.init(fileURLWithPath: singlePath)
                num += 1
                musicModel.musicNum=num;
                musicArryList.append(musicModel)
            }
            musicArry=musicArryList
            return musicArry!

        }else{
            return musicArry!
        }
    }

一個(gè)音樂(lè)播放器每次播放都只能放一首歌,于是我將其封裝成了一個(gè)AudioPlayer單例類卒落,其實(shí)也不能算單例羡铲,應(yīng)該叫工具類比較合適。

import UIKit
import AVFoundation
final class AudioPlayer: NSObject {
    private static var instance: AVAudioPlayer? = nil //static 直到被銷毀 全局存在
    private static var activeMusic:Music?=nil
    private static var isRandomPlay=false
    static func share(model:Music) -> Bool {
            do{
                try instance = AVAudioPlayer(contentsOf: model.musicURL!)
            }
            catch{
                instance=nil;
                print("error")
                return false;
            }
            instance?.play()
            activeMusic=model
            return true
    }
    //停止
    static func stop(){
        instance?.stop()
    }
    //播放
    static func play()->Bool{
        if (instance?.isPlaying)! {
            instance?.pause()
            return false
        }else{
            instance?.play()
            return true
        }
    }
    //暫停
    static func pause(){
        instance?.pause()
    }
    //下一曲
    static func nextsong( num:Int)->Bool{
        var num = num
        var musicArry:Array<Music>!
        musicArry=Music.getALL()
        if isRandomPlay{
           num = Int(arc4random_uniform(UInt32(musicArry.count-1)))
        }
        if(share(model: musicArry[num])){
            return true
        }else{
            return false
        }
    }
    //上一曲
    static func prevsong(num:Int)->Bool{
        var num = num
        var musicArry:Array<Music>!
        musicArry=Music.getALL()
        if isRandomPlay{
            num = Int(arc4random_uniform(UInt32(musicArry.count-1)))
        }
        if(share(model: musicArry[num])){
            return true
        }else{
            return false
        }
    }
    //聲音控制
    static func voice(num:Float){
        instance?.volume=num
    }
    //進(jìn)度條相關(guān)
    static func progress()->Double{
        return (instance?.currentTime)!/(instance?.duration)!
    }
    static func musicDuration()->Double{
        return (instance?.duration)!
    }
    
    static func currentTime()->Double{
        return (instance?.currentTime)!
    }
 
    //當(dāng)前播放的音樂(lè)
    static func activeSong()->Music?{
        return activeMusic
    }
    //是否在播放音樂(lè)
    static func isPlaying()->Bool{
        return (instance?.isPlaying)!
    }
    //隨機(jī)播放
    static func musicRandomPlay()->Bool{
        if  isRandomPlay==false{
            isRandomPlay=true
            return isRandomPlay
        }else{
            isRandomPlay=false
            return isRandomPlay
        }
    }
    
}

3.音樂(lè)播放

所有歌曲及其信息都能拿到了儡毕,展示在tableView上我就不用多說(shuō)也切,播放音樂(lè)也只需要在tableView的相應(yīng)點(diǎn)擊方法里使用AudioPlayer類的share方法將對(duì)應(yīng)的歌曲model傳入即可扑媚,這里有個(gè)如何讓miniplayer展示所點(diǎn)擊歌曲的問(wèn)題,我是將tableView放進(jìn)ContainerView雷恃,tableView相當(dāng)于主視圖的子視圖疆股,而miniplayer是在主視圖里,這里就要用到子視圖向父視圖回傳值的方法倒槐,在這里我是采用的閉包回傳旬痹,其實(shí)也不需要傳什么值,只需要讓主視圖知道子視圖被點(diǎn)擊了即可讨越,因?yàn)槲铱梢酝ㄟ^(guò)AudioPlayer類來(lái)得到當(dāng)前播放的歌曲信息两残。swift里的閉包傳值在語(yǔ)法上和oc的區(qū)別也是很大,花了我不少時(shí)間- -
子視圖里:

var musicPlayByTableViewCell: ((Int) -> Void)//聲明
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        musicPlayByTableViewCell?(indexPath.row)
        tableView.deselectRow(at: indexPath, animated: true)//取消被選中狀態(tài)
    }

主視圖里:

 self.musicTalbleVC.musicPlayByTableViewCell={
          [weak self]  musicNum in self?.musicModel=self?.musicArry[musicNum]
            self?.nowNum=musicNum
            self?.miniPlayerWork()
        }

3.1界面功能及相關(guān)細(xì)節(jié)

音樂(lè)播放時(shí)的界面也就是幾個(gè)button和常見(jiàn)的視圖控件把跨,說(shuō)說(shuō)進(jìn)度條人弓,AVAudioPlayer類提供了當(dāng)前進(jìn)度,以及總時(shí)長(zhǎng)着逐,所以我是用定時(shí)器每一秒獲取一次當(dāng)前進(jìn)度的方法票从,再當(dāng)前進(jìn)度除以總進(jìn)度使用即可,上下曲因?yàn)槲以讷@取所有歌曲時(shí)就將每首歌曲編了號(hào)滨嘱,上下曲就只需要將編號(hào)加一或減一就可以了峰鄙,包括隨機(jī)播放也只需要使用隨機(jī)函數(shù)隨機(jī)總歌曲編號(hào)就行了,還有些界面上的細(xì)節(jié)太雨,在詳細(xì)歌曲播放界面點(diǎn)擊了暫停吟榴,回到主視圖時(shí)miniplayer的圖標(biāo)及相關(guān)信息也應(yīng)該與其同步哦。tableView列表上也將當(dāng)前播放的歌曲進(jìn)行了高亮展示(加了歌活躍標(biāo)記)囊扳,在這里吩翻,我在musicmodel里添加了IsActive屬性,并在tableView的Cell里進(jìn)行了判斷锥咸,如果cell展示的歌曲是當(dāng)前正在播放的歌曲isActive為true展示活躍標(biāo)記狭瞎。還有音樂(lè)播放完畢后需要自動(dòng)播放下一曲哦,這里我是用的定時(shí)器控件搏予,讀取歌曲進(jìn)度進(jìn)度完了就播放下一曲(相當(dāng)點(diǎn)擊了一次下一曲按鈕)

3.12tabBar控制界面

在storyBoard上是直接拖的TabBar熊锭,和直接用tabBarController大不一樣,點(diǎn)擊不同的圖標(biāo)響應(yīng)不同的事件ContainerView里的視圖控制器也就相應(yīng)的變化雪侥,關(guān)于如何改變ContainerView里的視圖控制器我也是找了好久碗殷,還比較復(fù)雜,但確實(shí)ContainerView很好用八儆А锌妻!
ContainerView:更改視圖控制器

    func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        if item.tag == 1 {
            let newController = self.musicTalbleVC!
            let oldController = childViewControllers.last!
            if newController != oldController {
              //  self.setStatusBarBackgroundColor(color: UIColor.white)
                oldController.willMove(toParentViewController: nil)
                addChildViewController(newController)
                newController.view.frame = oldController.view.frame
                //isAnimating = true
                transition(from: oldController, to: newController, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromLeft, animations: nil, completion: { (finished) -> Void in
                    oldController.removeFromParentViewController()
                    newController.didMove(toParentViewController: self)                //self.isAnimating = false
                })
            }
        }else{
            let newController = self.moreVC!
            let oldController = childViewControllers.last!
             if newController != oldController {
                self.setStatusBarBackgroundColor(color: UIColor.clear)
                oldController.willMove(toParentViewController: nil)
                addChildViewController(newController)
                newController.view.frame = oldController.view.frame
                transition(from: oldController, to: newController, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromRight, animations: nil, completion: { (finished) -> Void in
                    oldController.removeFromParentViewController()
                    newController.didMove(toParentViewController: self)                //self.isAnimating = false
                })
            }
        }
    }

3.13音樂(lè)的后臺(tái)播放以及鎖屏和上拉欄展示音樂(lè)信息

音樂(lè)的后臺(tái)播放,因?yàn)槲疫€從為接觸過(guò)應(yīng)用后臺(tái)運(yùn)行這一塊旬牲,所以我也只是參照網(wǎng)上的代碼將功能做出來(lái)了而已仿粹,還有不完善的地方搁吓,后臺(tái)播放中斷(如打電話)后不會(huì)恢復(fù)播放。上拉欄展示音樂(lè)信息以及之中的按鈕觸發(fā)方法吭历,都是系統(tǒng)封裝好了的 直接用就是了擎浴。
話說(shuō)很多警告呀!

    func setBackground() {
        //大標(biāo)題 - 小標(biāo)題  - 歌曲總時(shí)長(zhǎng) - 歌曲當(dāng)前播放時(shí)長(zhǎng) - 封面
        self.musicModel=AudioPlayer.activeSong()
        var settings = [MPMediaItemPropertyTitle: self.musicModel?.musicName,
                        MPMediaItemPropertyArtist: self.musicModel?.musicAuthor ,
                        MPMediaItemPropertyPlaybackDuration: "\(AudioPlayer.musicDuration())",
            MPNowPlayingInfoPropertyElapsedPlaybackTime: "\(AudioPlayer.currentTime())",MPMediaItemPropertyArtwork: MPMediaItemArtwork.init(image: UIImage (data: (self.musicModel?.musicimg)!)!)] as [String : Any]
        MPNowPlayingInfoCenter.default().setValue(settings, forKey: "nowPlayingInfo")
        if AudioPlayer.progress() > 0.99 { // 自動(dòng)播放下一曲 ()后臺(tái)
                self.nowNum=self.nowNum+1
            if self.nowNum>=self.musicArry.count{
                self.nowNum=0
            }
            if AudioPlayer.nextsong(num: self.nowNum){
                refreashView()
            }
        }
    }

4.最后

這是剛?cè)胄械牟穗u的第一篇博客毒涧,肯定有很多不妥當(dāng)?shù)牡胤街ぃM蠹乙?jiàn)諒,項(xiàng)目也沒(méi)做多久契讲,對(duì)swift的理解還很淺仿吞,肯定不如大大們的法眼,我也只希望通過(guò)寫這篇博客捡偏,總結(jié)一下我學(xué)到的知識(shí)唤冈,分享出來(lái),大家互相交流學(xué)習(xí)银伟。

附源碼 https://github.com/calvinWen/SwiftMusicPlayer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末你虹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子彤避,更是在濱河造成了極大的恐慌傅物,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琉预,死亡現(xiàn)場(chǎng)離奇詭異董饰,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)圆米,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門卒暂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人娄帖,你說(shuō)我怎么就攤上這事也祠。” “怎么了近速?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵诈嘿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我数焊,道長(zhǎng)永淌,這世上最難降的妖魔是什么崎场? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任佩耳,我火速辦了婚禮,結(jié)果婚禮上谭跨,老公的妹妹穿的比我還像新娘干厚。我一直安慰自己李滴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布蛮瞄。 她就那樣靜靜地躺著所坯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪挂捅。 梳的紋絲不亂的頭發(fā)上芹助,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音闲先,去河邊找鬼状土。 笑死,一個(gè)胖子當(dāng)著我的面吹牛伺糠,可吹牛的內(nèi)容都是我干的蒙谓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼训桶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼累驮!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起舵揭,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤谤专,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后午绳,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毒租,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年箱叁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了墅垮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耕漱,死狀恐怖算色,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情螟够,我是刑警寧澤灾梦,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站妓笙,受9級(jí)特大地震影響若河,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寞宫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一萧福、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辈赋,春花似錦鲫忍、人聲如沸膏燕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坝辫。三九已至,卻和暖如春射亏,著一層夾襖步出監(jiān)牢的瞬間近忙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工智润, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留银锻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓做鹰,卻偏偏與公主長(zhǎng)得像击纬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钾麸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容