iOS音訊播放 (六):簡單的音訊播放器實現
在前幾篇中我分別講到了AudioSession
、AudioFileStream
、AudioFile
、AudioQueue
,這些類的功能已經涵蓋了第一篇中所提到的音訊播放所需要的步驟:
- 讀取MP3檔案
NSFileHandle
- 解析取樣率、位元速率、時長等資訊,分離MP3中的音訊幀
AudioFileStream
/AudioFile
- 對分離出來的音訊幀解碼得到PCM資料
AudioQueue
對PCM資料進行音效處理(均衡器、混響器等,非必須)省略
- 把PCM資料解碼成音訊訊號
AudioQueue
- 把音訊訊號交給硬體播放
AudioQueue
- 重複1-6步直到播放完成
下面我們就講講述如何用這些部件組成一個簡單的本地音樂播放器
AudioFileStream vs AudioFile
解釋一下為什麼我要同時使用AudioFileStream和AudioFile。
第一,對於網路流播必須有AudioFileStream的支援
,這是因為我們在第四篇中提到過AudioFile在Open時會要求使用者提供資料,如果提供的資料不足會直接跳過並且返回錯誤碼,而資料不足的情況在網路流中很常見,故無法使用AudioFile單獨進行網路流資料的解析;
第二,對於本地音樂播放選用AudioFile更為合適
,原因如下:
- AudioFileStream的主要是用在流播放中雖然不限於網路流和本地流,但流資料是按順序提供的所以AudioFileStream也是順序解析的,被解析的音訊檔案還是需要符合流播放的特性,對於不符合的本地檔案AudioFileStream會在Parse時返回
NotOptimized
錯誤; - AudioFile的解析過程並不是順序的,它會在解析時通過回撥向使用者索要某個位置的資料,即使資料在檔案末尾也不要緊,所以AudioFile適用於所有型別的音訊檔案;
基於以上兩點我們可以得出這樣一個結論:一款完整功能的播放器應當同時使用AudioFileStream和AudioFile
,用AudioFileStream
本來這個Demo應該做成基於網路流的音訊播放,但由於最近比較忙一直過著公司和床兩點一線的生活,來不及寫網路流和檔案快取的模組,所以就用本地檔案代替了,所以最終在Demo會先嚐試用AudioFileStream解析資料,如果失敗再嘗試使用AudioFile以達到模擬網路流播放的效果。
準備工作
第一件事當然是要建立一個新工程,這裡我選擇了的模板是SingleView,工程名我把它命名為MCSimpleAudioPlayerDemo
:
建立完工程之後去到Target屬性的Capabilities
選項卡設定Background
Modes
,把Audio and Airplay
勾選,這樣我們的App就可以在進入後臺之後繼續播放音樂了:
接下來我們需要搭建一個簡單的UI,在storyboard上建立兩個UIButton和一個UISlider,Button用來做播放器的播放、暫停、停止等功能控制,Slider用來顯示播放進度和seek。把這些UI元件和ViewController的屬性/方法關聯上之後簡單的UI也就完成了。
介面定義
下面來建立播放器類MCSimpleAudioPlayer
,首先是初始化方法(感謝@喵神的VVDocumenter):
1 2 3 4 5 6 7 8 9 |
|
另外播放器作為一個典型的狀態機,各種狀態也是必不可少的,這裡我只簡單的定義了四種狀態:
1 2 3 4 5 6 7 |
|
再加上一些必不可少的屬性和方法組成了MCSimpleAudioPlayer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
初始化
在init方法中建立一個NSFileHandle的例項以用來讀取資料並交給AudioFileStream解析,另外也可以根據生成的例項是否是nil來判斷是否能夠讀取檔案,如果返回的是nil就說明檔案不存在或者沒有許可權那麼播放也就無從談起了。
1
|
|
通過NSFileManager獲取檔案大小
1
|
|
初始化方法到這裡就結束了,作為一個播放器我們自然不能在主執行緒進行播放,我們需要建立自己的播放執行緒。
建立一個成員變數_started
來表示播放流程是否已經開始,在-play
方法中如果_started
為NO就建立執行緒_thread
並以-threadMain
方法作為main,否則說明執行緒已經建立並且在播放流程中:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
接下來就可以在-threadMain
進行音訊播放相關的操作了。
建立AudioSession
iOS音訊播放的第一步,自然是要建立AudioSession
,這裡引入第二篇末尾給出的AudioSession封裝MCAudioSession,當然各位也可以使用AVAudioSession
。
初始化的工作會在呼叫單例方法時進行,下一步是設定Category。
1 2 |
|
成功之後啟用AudioSession,還有別忘了監聽Interrupt通知。
1 2 3 4 5 6 7 8 9 |
|
讀取、解析音訊資料
成功建立並啟用AudioSession之後就可以進入播放流程了,播放是一個無限迴圈的過程,所以我們需要一個while迴圈,在檔案沒有被播放完成之前需要反覆的讀取、解析、播放。那麼第一步是需要讀取並解析資料。按照之前說的我們會先使用AudioFileStream
,引入第三篇末尾給出的AudioFileStream封裝MCAudioFileStream。
建立AudioFileStream,MCAudioFileStream的init方法會完成這項工作,如果建立成功就設定delegate作為Parse資料的回撥。
1 2 3 4 5 |
|
接下來要讀取資料並且解析,用成員變數_offset
表示_fileHandler
已經讀取檔案位置,其主要作用是來判斷Eof。呼叫MCAudioFileStream的-parseData:error:
方法來對資料進行解析。
1 2 3 4 5 6 7 8 9 10 11 |
|
解析完檔案頭之後MCAudioFileStream的readyToProducePackets
屬性會被置為YES,此後所有的Parse方法都回觸發-audioFileStream:audioDataParsed:
方法並傳遞MCParsedAudioData
的陣列來儲存解析完成的資料。這樣就需要一個buffer來儲存這些解析完成的音訊資料。
於是建立了MCAudioBuffer
類來管理所有解析完成的資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
建立一個MCAudioBuffer的例項_buffer
,解析完成的資料都會通過enqueue
方法儲存到_buffer中,在需要的使用可以通過dequeue
取出來使用。
1 2 3 4 5 6 7 |
|
如果遇到AudioFileStream解析失敗的話,轉而使用AudioFile,引入第四篇末尾給出的AudioFile封裝MCAudioFile(之前沒有給出,最近補上的)。
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
使用AudioFile時同樣需要NSFileHandle來讀取檔案資料,但由於其回獲取資料的特性我把FileHandle的相關操作都封裝進去了,所以使用MCAudioFile解析資料時直接呼叫Parse方法即可。
播放
有了解析完成的資料,接下來就該AudioQueue出場了,引入第五篇末尾提到的AudioQueue的封裝MCAudioOutputQueue。
首先建立AudioQueue,由於AudioQueue需要實現建立重用buffer所以需要事先確定bufferSize,這裡我設定的bufferSize是近似0.1秒的資料量,計算bufferSize需要用到的duration和audioDataByteCount可以從MCAudioFileStream或者MCAudioFile中獲取。有了bufferSize之後,加上資料格式format引數和magicCookie(部分音訊格式需要)就可以生成AudioQueue了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
接下來從_buffer中讀出解析完成的資料,交給AudioQueue播放。如果全部播放完畢了就呼叫一下-flush
讓AudioQueue把剩餘資料播放完畢。這裡需要注意的是MCAudioOutputQueue的-playData
方法在呼叫時如果沒有可以重用的buffer的話會阻塞當前執行緒直到AudioQueue
回撥方法送出可重用的buffer為止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
暫停 & 恢復
暫停方法很簡單,呼叫MCAudioOutputQueue的-pause
方法就可以了,但要注意的是需要和-playData:
同步呼叫,否則可能引起一些問題(比如觸發了pause實際由於併發操作沒有真正pause住)。
同步的方法可以採用加鎖的方式,也可以通過標誌位在threadMain
中進行Pause,Demo中我使用了後者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
在暫停後還要記得阻塞執行緒。
恢復只要呼叫AudioQueue start方法就可以了,同時記得signal讓執行緒繼續跑
1 2 3 4 5 6 |
|
播放進度 & Seek
對於播放進度我在第五篇講AudioQueue時已經提到過了,使用AudioQueueGetCurrentTime
方法可以獲取實際播放的時間
如果Seek之後需要根據計算timingOffset,然後根據timeOffset來計算最終的播放進度:
1 2 3 4 |
|
timingOffset的計算在Seek進行,Seek操作和暫停操作一樣需要和其他AudioQueue的操作同步進行,否則可能造成一些併發問題。
1 2 3 4 5 6 |
|
在seek時為了防止播放進度跳動,修改一下獲取播放進度的方法:
1 2 3 4 5 6 7 8 |
|
下面是threadMain
中的Seek操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Seek時需要做如下事情:
- 計算timingOffset
- 清除之前殘餘在
_buffer
中的資料 - 挪動NSFileHandle的遊標
- 清除AudioQueue中已經Enqueue的資料
- 如果有用到音效器的還需要清除音效器裡的殘餘資料
打斷
在接到Interrupt通知時需要處理打斷,下面是打斷的處理方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
這裡需要注意,打斷操作我放在了主執行緒進行而並非放到新開的執行緒中進行,原因如下:
-
一旦打斷開始AudioSession被搶佔後音訊立即被打斷,此時AudioQueue的所有操作會暫停,這就意味著不會有任何資料消耗回撥產生;
-
我這個Demo的執行緒模型中在向AudioQueue Enqueue了足夠多的資料之後會阻塞當前執行緒等待資料消耗的回撥才會signal讓執行緒繼續跑;
於是就得到了這樣的結論:一旦打斷開始我建立的執行緒就會被阻塞,所以我需要在主執行緒來處理暫停和恢復播放。
停止 & 清理
停止操作也和其他操作一樣會放到threadMain
中執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|