IOS音視頻(三)AVFoundation 播放和錄音git
IOS音視頻(四十三)AVFoundation 之 Audio Sessiongithub
IOS音視頻(四十四)AVFoundation 之 Audio Queue Services算法
IOS音視頻(四十五)HTTPS 自簽名證書 實現邊下邊播swift
IOS音視頻(三)AVFoundation 播放和錄音網絡
回顧一下,上一篇博客「IOS音視頻(二)AVFoundation視頻捕捉」 中講解了關於AVFoundation框架對攝像頭視頻的捕捉能力,並用兩個demo(一個OC的Demo,一個Swift的Demo)詳細講解了AVFoundation處理攝像頭視頻捕捉的能力,能夠捕捉靜態圖片,也能夠捕捉實時視頻流,能夠錄製視頻,還提供了接口操做閃光燈,開啓手電筒模式等等功能。可是這些講解都是基於蘋果官方文檔一些接口講解的,學習了這些咱們雖然知道了怎麼調用蘋果的接口實現相關功能,可是咱們並不知道其中的原理性知識,後續的博客中將從視頻採集,視頻編碼,視頻解碼等原理方面詳細講解,因爲時間問題,寫完一篇博客基本上要花費一天的時間,因此進度有些慢,博客中也參考了不少大神的博客,可是這些博客是咱們平時收集在印象筆記中的,可能有時候忘記添加原始地址了,後續有時間會補上。session
感受說了好多廢話,好了,先簡單介紹一下:本篇博客主要講解AVFoundation在音頻處理方面的能力。app
在音頻方面,咱們主要是指錄製音頻和播放音頻兩個重要的能力,在AVFoundation框架中,等爲咱們提供了相關類,很容易就實現這些功能。可是咱們須要理解一下原理性的知識,便於咱們開發中遇到問題就能夠及時解決。
在開始講解錄音和播放音頻以前,有必要學習一下音頻的一些理論知識,方便咱們更好的理解。
本篇博客的錄音Demo點擊這裏下載:AVFoundation錄音Demo swift版本, AVFoundation錄音播放demo OC版本
聲音是如何產生的呢?
- 聲音是有物體振動而產生的。 如圖所示,當小球撞擊到音叉的時候,音叉會產生振動,對周圍的空氣產生擠壓,從而產生聲音。聲音是一種壓力波,當演奏樂器、拍打一扇門或者敲擊桌面時,它們的振動都會引發空氣有節奏的振動,使周圍的空氣產生疏密變化,造成疏密相間的縱波(能夠理解爲石頭落入水中激起的波紋),由此就產生了聲波,這種現象會一直延續到振動消失爲止。
- 聲波的三要素是頻率、振幅和波形,頻率表明音階的高低,振幅表明響度,波形表明音色。
- 頻率(過零率)越高,波長就越短。低頻聲響的波長則較長,因此其能夠更容易地繞過障礙物,所以能量衰減就小,聲音就會傳得遠,反之則會獲得徹底相反的結論。
- 響度其實就是能量大小的反映,用不一樣的力度敲擊桌子,聲音的大小勢必也會不一樣。在生活中,分貝經常使用於描述響度的大小。聲音超過必定的分貝,人類的耳朵就會受不了。
人類耳朵的聽力有一個頻率範圍,大約是
20Hz~20kHz
,不過,即便是在這個頻率範圍內,不一樣的頻率,聽力的感受也會不同,業界很是著名的等響曲線,就是用來描述等響條件下聲壓級與聲波頻率關係的,人耳對3~4kHz
頻率範圍內的聲音比較敏感,而對於較低或較高頻率的聲音,敏感度就會有所減弱;在聲壓級較低時,聽覺的頻率特性會很不均勻;而在聲壓級較高時,聽覺的頻率特性會變得較爲均勻。頻率範圍較寬的音樂,其聲壓以80~90dB爲最佳,超過90dB將會損害人耳(105dB爲人耳極限)。
吉他是經過演奏者撥動琴絃來發出聲音的,鼓是經過鼓槌敲擊鼓面發出聲音的,這些聲音的產生都離不開振動,就連咱們說話也是由於聲帶振動而產生聲音的。既然都是振動產生的聲音,那爲何吉他、鼓和人聲聽起來相差這麼大呢?這是由於介質不一樣。咱們的聲帶振動發出聲音以後,通過口腔、顱腔等局部區域的反射,再通過空氣傳播到別人的耳朵裏,這就是咱們說的話被別人聽到的過程,其中包括了最初的發聲介質與顱腔、口腔,還有中間的傳播介質等。事實上,聲音的傳播介質很廣,它能夠經過空氣、液體和固體進行傳播;並且介質不一樣,傳播的速度也不一樣,好比,聲音在空氣中的傳播速度爲340m/s,在蒸餾水中的傳播速度爲1497m/s,而在鐵棒中的傳播速度則能夠高達5200m/s;不過,聲音在真空中是沒法傳播的。
- 吸音主要是解決聲音反射而產生的嘈雜感,吸音材料能夠衰減入射音源的反射能量,從而達到對原有聲源的保真效果,好比錄音棚裏面的牆壁上就會使用吸音棉材料。
- 隔音主要是解決聲音的透射而下降主體空間內的吵鬧感,隔音棉材料能夠衰減入射音源的透射能量,從而達到主體空間的安靜狀態,好比KTV裏面的牆壁上就會安裝隔音棉材料。
當咱們在高山或空曠地帶高聲大喊的時候,常常會聽到回聲(echo)。之因此會有回聲是由於聲音在傳播過程當中遇到障礙物會反彈回來,再次被咱們聽到。可是,若兩種聲音傳到咱們的耳朵裏的時差小於80毫秒,咱們就沒法區分開這兩種聲音了,其實在平常生活中,人耳也在收集回聲,只不過因爲嘈雜的外界環境以及回聲的分貝(衡量聲音能量值大小的單位)比較低,因此咱們的耳朵分辨不出這樣的聲音,或者說是大腦能接收到但分辨不出。
天然界中有光能、水能,生活中有機械能、電能,其實聲音也能夠產生能量,例如兩個頻率相同的物體,敲擊其中一個物體時另外一個物體也會振動發聲。這種現象稱爲共鳴,共鳴證實了聲音傳播能夠帶動另外一個物體振動,也就是說,聲音的傳播過程也是一種能量的傳播過程。
爲了將模擬信號數字化,須要3個過程分別是採樣、量化和編碼。
首先要對模擬信號進行採樣,所謂採樣就是在時間軸上對信號進行數字化。根據奈奎斯特定理(也稱爲採樣定理),按比聲音最高頻率高2倍以上的頻率對聲音進行採樣(也稱爲AD轉換)。
對於高質量的音頻信號,其頻率範圍(人耳可以聽到的頻率範圍)是20Hz~20kHz,因此採樣頻率通常爲44.1kHz,這樣就能夠保證採樣聲音達到20kHz也能被數字化,從而使得通過數字化處理以後,人耳聽到的聲音質量不會被下降。而所謂的44.1kHz就是表明1秒會採樣44100次。
那麼,具體的每一個採樣又該如何表示呢?
這就是量化。
量化是指在幅度軸上對信號進行數字化,好比用16比特的二進制信號來表示聲音的一個採樣,而16比特(一個short)所表示的範圍是[-32768,32767],共有65536個可能取值,所以最終模擬的音頻信號在幅度上也分爲了65536層。以下圖所示:
既然每個量化都是一個採樣,那麼這麼多的採樣該如何進行存儲呢?
這就須要-編碼。所謂編碼,就是按照必定的格式記錄採樣和量化後的數字數據,好比順序存儲或壓縮存儲,等等。
這裏面涉及了不少種格式,一般所說的音頻的裸數據格式就是脈衝編碼調製(Pulse Code Modulation,PCM)數據。描述一段PCM數據通常須要如下幾個概念:量化格式(sampleFormat)、採樣率(sampleRate)、聲道數(channel)。以CD的音質爲例:量化格式(有的地方描述爲位深度)爲16比特(2字節),採樣率爲44100,聲道數爲2,這些信息就描述了CD的音質。
而對於聲音格式,還有一個概念用來描述它的大小,稱爲數據比特率,即1秒時間內的比特數目,它用於衡量音頻數據單位時間內的容量大小。
而對於CD音質的數據,比特率爲多少呢?
計算以下: 44100 * 16 * 2 = […]
- 計算以下: 1378.125 * 60 / 8 / 1024 = 10.09MB
- 若是sampleFormat更加精確(好比用4字節來描述一個採樣),或者sampleRate更加密集(好比48kHz的採樣率),那麼所佔的存儲空間就會更大,同時可以描述的聲音細節就會越精確。存儲的這段二進制數據即表示將模擬信號轉換爲數字信號了,之後就能夠對這段二進制數據進行存儲、播放、複製,或者進行其餘任何操做。
麥克風裏面有一層碳膜,很是薄並且十分敏感。前面介紹過,聲音實際上是一種縱波,會壓縮空氣也會壓縮這層碳膜,碳膜在受到擠壓時也會發出振動,在碳膜的下方就是一個電極,碳膜在振動的時候會接觸電極,接觸時間的長短和頻率與聲波的振動幅度和頻率有關,這樣就完成了聲音信號到電信號的轉換。以後再通過放大電路處理,就能夠實施後面的採樣量化處理了。
分貝是用來表示聲音強度的單位。平常生活中聽到的聲音,若以聲壓值來表示,因爲其變化範圍很是大,能夠達到六個數量級以上,同時因爲咱們的耳朵對聲音信號強弱刺激的反應不是線性的,而是呈對數比例關係,因此引入分貝的概念來表達聲學量值。所謂分貝是指兩個相同的物理量(例如,A1和A0)之比取以10爲底的對數並乘以10(或20),即:
N= 10 * lg(A1 / A0)
分貝符號爲「dB」,它是無量綱的。式中A0是基準量(或參考量),A1是被量度量。
有損壓縮
和無損壓縮
。
- 無損壓縮是指解壓後的數據能夠徹底復原。在經常使用的壓縮格式中,用得較多的是有損壓縮,
- 有損壓縮是指解壓後的數據不能徹底復原,會丟失一部分信息,壓縮比越小,丟失的信息就越多,信號還原後的失真就會越大。根據不一樣的應用場景(包括存儲設備、傳輸網絡環境、播放設備等),能夠選用不一樣的壓縮編碼算法,如PCM、WAV、AAC、MP三、Ogg等。
壓縮編碼的原理:其實是壓縮掉冗餘信號,冗餘信號是指不能被人耳感知到的信號,包含人耳聽覺範圍以外的音頻信號以及被掩蔽掉的音頻信號等。人耳聽覺範圍以外的音頻信號在前面已經提到過,因此在此再也不贅述。而被掩蔽掉的音頻信號則主要是由於人耳的掩蔽效應,主要表現爲頻域掩蔽效應與時域掩蔽效應,不管是在時域仍是頻域上,被掩蔽掉的聲音信號都被認爲是冗餘信息,不進行編碼處理。
主要有:WAV編碼, MP3編碼, AAC編碼, Ogg編碼。
PCM
(脈衝編碼調製)是Pulse Code Modulation的縮寫。前面已經介紹過PCM大體的工做流程,而WAV編碼的一種實現(有多種實現方式,可是都不會進行壓縮操做)就是在PCM數據格式的前面加上44字節,分別用來描述PCM
的採樣率、聲道數、數據格式等信息。- 特色:音質很是好,大量軟件都支持。
- 適用場合:多媒體開發的中間文件、保存音樂和音效素材。
MP3
具備不錯的壓縮比,使用LAME
編碼(MP3
編碼格式的一種實現)的中高碼率的MP3
文件,聽感上很是接近源WAV文件,固然在不一樣的應用場景下,應該調整合適的參數以達到最好的效果。- 特色:音質在128Kbit/s以上表現還不錯,壓縮比比較高,大量軟件和硬件都支持,兼容性好。
- 適用場合:高比特率下對兼容性有要求的音樂欣賞。
AAC
是新一代的音頻有損壓縮技術,它經過一些附加的編碼技術(好比PS、SBR等),衍生出了LC-AAC
、HE-AAC
、HE-AAC v2
三種主要的編碼格式。LC-AAC是比較傳統的AAC,相對而言,其主要應用於中高碼率場景的編碼(≥80Kbit/s);HE-AAC(至關於AAC+SBR)主要應用於中低碼率場景的編碼(≤80Kbit/s);而新近推出的HE-AAC v2(至關於AAC+SBR+PS)主要應用於低碼率場景的編碼(≤48Kbit/s)。事實上大部分編碼器都設置爲≤48Kbit/s自動啓用PS技術,而>48Kbit/s則不加PS,至關於普通的HE-AAC。- 特色:在小於128Kbit/s的碼率下表現優異,而且多用於視頻中的音頻編碼。
- 適用場合:128Kbit/s如下的音頻編碼,多用於視頻中音頻軌的編碼。
Ogg
是一種很是有潛力的編碼,在各類碼率下都有比較優秀的表現,尤爲是在中低碼率場景下。Ogg
除了音質好以外,仍是徹底免費的,這爲Ogg
得到更多的支持打好了基礎。Ogg
有着很是出色的算法,能夠用更小的碼率達到更好的音質,128Kbit/s的Ogg比192Kbit/s甚至更高碼率的MP3還要出色。但目前由於尚未媒體服務軟件的支持,所以基於Ogg的數字廣播還沒法實現。Ogg
目前受支持的狀況還不夠好,不管是軟件上的仍是硬件上的支持,都沒法和MP3相提並論。- 特色:能夠用比MP3更小的碼率實現比MP3更好的音質,高中低碼率下均有良好的表現,兼容性不夠好,流媒體特性不支持。
- 適用場合:語音聊天的音頻消息場景。
只要是Core Audio框架支持的音頻編解碼, AVFoundation框架均可以支持, 這意味着 AVFoundation可以支持大量不一樣格式的資源。然而在不用線性PCM音頻的狀況下,更多的只能使用AAC。
高級音頻編碼AAC是H.264標準相應的音頻處理方式,目前已經成爲音頻流和下載的音頻資源中最主流的編碼方式。這種格式比MP3格式有着顯著的提高,能夠在低比特率的前提下提供更高質量的音頻,是在Web上發佈和傳播的音頻格式中最爲理想的。此外,AAC沒有來自證書和許可方面的限制,這一限制曾經在MP3格式上飽受詬病。
AVFoundation和Core Audio框架都提供對MP3數據解碼支持,可是不支持對其進行編碼。
AVFoundation框架最開始是一個僅針對音頻的框架,該框架的前身在IOS2.2版本中引入,只包含一個出來音頻播放的類;在iOS 3.0中,蘋果公司增長了音頻錄製功能。雖然這些類都是目前該框架中最古老的,但他們任然是最經常使用的幾個類。
上面講解了這麼多音頻理論知識,接下來咱們將從 AVFoundation的兩個基礎類AVAudioPlayer和AVAudioRecorder來說解音頻播放和音頻錄製功能。
- 播聽任何持續時間的聲音
- 播放來自文件或內存緩衝區的聲音
- 循環播放
- 同時播放多個聲音,每一個音頻播放器一個聲音,精確同步
- 控制相對播放級別、立體聲定位和播放速度
- 查找聲音文件中的特定點,該點支持快進和快退等應用程序特性.
- 獲取可用於回放級別測量的數據.
(1)當音頻播放完成時,會調用下面的回調方法:
optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
複製代碼
(2)當音頻播放器在播放過程當中遇到解碼錯誤時會調用下面這個回調方法:
optional func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?)
複製代碼
//異步播放聲音。
func play() -> Bool
//以異步方式播放聲音,從音頻輸出設備時間軸中的指定點開始播放。
func play(atTime: TimeInterval) -> Bool
//暫停播放;聲音準備好從它中止的地方恢復播放。
func pause()
//中止播放並撤消播放所需的設置。
func stop()
//經過預加載音頻播放器的緩衝區來準備播放。
func prepareToPlay() -> Bool
//淡入到一個新的卷在一個特定的持續時間。
func setVolume(Float, fadeDuration: TimeInterval)
//一個布爾值,指示音頻播放器是否正在播放(真)或不(假)。
var isPlaying: Bool
//音頻播放器的播放音量,線性範圍從0.0到1.0。
var volume: Float
//音頻播放器的立體聲平移位置。
var pan: Float
//音頻播放器的播放速率。
var rate: Float
//一個布爾值,用於指定是否爲音頻播放器啓用播放速率調整。
var enableRate: Bool
//一個聲音返回到開始的次數,到達結束時,重複播放。
var numberOfLoops: Int
//音頻播放器的委託對象。
var delegate: AVAudioPlayerDelegate?
//一種協議,它容許一個委託響應音頻中斷和音頻解碼錯誤,並完成聲音的回放。
protocol AVAudioPlayerDelegate //音頻播放器的設置字典,包含與播放器相關的聲音信息。 var settings: [String : Any] 複製代碼
//聲音中與音頻播放器相關聯的音頻通道的數量。
var numberOfChannels: Int
//與音頻播放器相關聯的AVAudioSessionChannelDescription對象的數組
var channelAssignments: [AVAudioSessionChannelDescription]?
//與音頻播放器相關聯的聲音的總持續時間(以秒爲單位).
var duration: TimeInterval
//播放點,以秒爲單位,在與音頻播放器關聯的聲音的時間軸內。
var currentTime: TimeInterval
//音頻輸出設備的時間值,以秒爲單位。
var deviceCurrentTime: TimeInterval
//與音頻播放器關聯的聲音的URL。
var url: URL?
//包含與音頻播放器相關聯的聲音的數據對象。
var data: Data?
//當前音頻播放器的UID。
var currentDevice: String?
//緩衝區中音頻的格式。
var format: AVAudioFormat
複製代碼
//一個布爾值,用於指定音頻播放器的音頻電平測量開/關狀態。
var isMeteringEnabled: Bool
//返回給定頻道的平均功率(以分貝爲單位)。
func averagePower(forChannel: Int) -> Float
//返回給定頻道的峯值功率,以分貝表示所播放的聲音。
func peakPower(forChannel: Int) -> Float
//返回刷新音頻播放器全部頻道的平均和峯值功率值。
func updateMeters()
複製代碼
//格式標識符。
let AVFormatIDKey: String
//採樣率,用赫茲表示,表示爲NSNumber浮點值。通常爲8000,和16K
let AVSampleRateKey: String
//用NSNumber整數值表示的通道數。
let AVNumberOfChannelsKey: String
複製代碼
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
if (self.player) {
[self.player prepareToPlay];
}
複製代碼
建立出 AVAudioPlayer 後建議調用 prepareToPlay 方法,這個方法會取得須要的音頻硬件並預加載 Audio Queue 的緩衝區,固然若是不主動調用,執行 play 方法時也會默認調用,可是會形成輕微播放的延時。
AVAudioPlayer 的 play 能夠播放音頻,stop 和 pause 均可以暫停播放,可是 stop 會撤銷調用 prepareToPlay 所作的設置。從上面介紹的AVAudioPlayer屬性能夠知道如何設置。具體設置 以下:
- 修改播放器的音量:播放器音量獨立於系統音量,音量或播放增益定義爲 0.0(靜音)到 1.0(最大音量)之間的浮點值
- 修改播放器的 pan 值:容許使用立體聲播放聲音,pan 值從 -1.0(極左)到 1.0(極右),默認值 0.0(居中)
- 調整播放率:0.5(半速)到 2.0(2 倍速)
- 設置 numberOfLoops 實現無縫循環:-1 表示無限循環(音頻循環能夠是未壓縮的線性 PCM 音頻,也能夠是 AAC 之類的壓縮格式音頻,MP3 格式不推薦循環)
- 音頻計量:當播放發生時從播放器讀取音量力度的平均值和峯值
NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
for (AVAudioPlayer *player in self.players) {
[player playAtTime:delayTime];
}
self.playing = YES;
複製代碼
對於多個須要播放的音頻,若是但願同步播放效果,則須要捕捉當前設備時間並添加一個小延時,從而具備一個從開始播放時間計算的參照時間。deviveCurrentTime 是一個獨立於系統事件的音頻設備的時間值,當有多於 audioPlayer 處於 play 或者 pause 狀態時 deviveCurrentTime 會單調增長,沒有時置位爲 0。playAtTime 的參數 time 要求必須是基於 deviveCurrentTime 且大於等於 deviveCurrentTime 的時間。
for (AVAudioPlayer *player in self.players) {
[player stop];
player.currentTime = 0.0f;
}
複製代碼
暫停時須要將 audioPlayer 的 currentTime 值設置爲 0.0,當音頻正在播放時,這個值用於標識當前播放位置的偏移,不播放音頻時標識從新播放音頻的起始偏移。
play
方法能夠實現當即播放音頻的功能,pause
方法能夠對播放暫停,那麼可想而知stop
方法能夠中止播放行爲。有趣的是,pause
和stop
方法在應用程序外面看來實現的功能都是中止當前播放行爲。下一時間裏咱們調用play
方法,經過pause
和stop
方法中止的音頻都會繼續播放。prepareToPlay
時所作的設置,而調用pause方法則不會。player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
複製代碼
因爲音頻會話是全部應用公用的,全部通常在程序啓動時設置,是經過AVAudioSession單例來設置的。
若是但願應用程序播放音頻時屏蔽靜音切換動做,須要設置會話分類爲 AVAudioSessionCategoryPlayback,可是若是但願按下鎖屏後還能夠播放,就須要在 plist 里加入一個 Required background modes 類型的數組,在其中添加 App plays audio or streams audio/video using AirPlay。
後面講解錄音時還會詳細講解配置音頻會話。
- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);
- (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);
複製代碼
中斷結束調用的方法會帶入一個 options 參數,若是是 AVAudioSessionInterruptionOptionShouldResume
則代表能夠恢復播放音頻了。
在準備爲出現的中斷時間採起動做前,首先要獲得中斷出現的通知,註冊應用程序的AVAudioSession發送的通知AVAudioSessionInterruptionNofication.
override init() {
super.init()
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(handleInterruption(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
nc.addObserver(self, selector: #selector(handleRouteChange(_:)), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance())
}
複製代碼
推送的通知會包含一個帶有許多重要信息的userInfo字典,根據這個字典能夠肯定採起哪些適合的操做。以下代碼:
@objc func handleInterruption(_ notification: Notification) {
if let info = (notification as NSNotification).userInfo {
let type = info[AVAudioSessionInterruptionTypeKey] as! AVAudioSession.InterruptionType
if type == .began {
stop()
delegate?.playbackStopped()
} else {
let options = info[AVAudioSessionInterruptionOptionKey] as! AVAudioSession.InterruptionOptions
if options == .shouldResume {
play()
delegate?.playbackBegan()
}
}
}
}
複製代碼
在handleInterrupation方法中,首先經過檢索AVAudioSessionInterrupationTypeKey的值肯定中斷類型(type),咱們調用stop方法,並經過調用委託函數playbackStopped
方法向委託通知中斷狀態。很重要的一點是當通知被接收是,音頻會話已經被終止,且AVAudioPlayer實例處於暫停狀態。調用控制啓動stop方法只能更新內部狀態,並不能中止播放。
NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
[nsnc addObserver:self
selector:@selector(handleRouteChange:)
name:AVAudioSessionRouteChangeNotification
object:[AVAudioSession sharedInstance]];
複製代碼
- (void)handleRouteChange:(NSNotification *)notification {
NSDictionary *info = notification.userInfo;
AVAudioSessionRouteChangeReason reason =
[info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
AVAudioSessionRouteDescription *previousRoute =
info[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
NSString *portType = previousOutput.portType;
if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
[self stop];
[self.delegate playbackStopped];
}
}
}
複製代碼
接收到通知後要作的第一件事情是判斷線路變動發生的緣由。查看保存userinfo字典中的表示緣由的AVAudioSessionRouteChangeReasonKey
值。這個返回值是一個用於表示變化緣由的無符號整數。經過緣由能夠推斷出不一樣的事件。好比有新設備接入或者改變音頻會話類型,不過咱們須要特殊注意的是耳機短褲這個事件,這個事件的對應緣由爲:AVAudioSessionRouteChangeReasonOldDeviceUnavailable
知道有設備斷開鏈接後,須要向userinfo字典提出請求,以得到其中用於描述前一個線路的AVAudioSessionPortDescription
。線路的描述信息是整合在一個熟人NSArray和一個輸出NSArray中。在上述狀況下,你須要從線路描述中找出第一個輸出接口並判斷其是否爲耳機接口。若是是,則中止播放,並調用委託函數的playbackStopeed方法。
這裏 AVAudioSessionPortHeadphones 只包含了有線耳機,無線藍牙耳機須要判斷 AVAudioSessionPortBluetoothA2DP 值。
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@property (nonatomic,strong)AVAudioPlayer *player;
@end
@implementation ViewController
-(AVAudioPlayer *)player{
if (_player == nil) {
//1.音樂資源
NSURL *url = [[NSBundle mainBundle]URLForResource:@"235319.mp3" withExtension:nil];
//2.建立AVAudioPlayer對象
_player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
//3.準備播放(緩衝,提升播放的流暢性)
[_player prepareToPlay];
}
return _player;
}
//播放(異步播放)
- (IBAction)play {
[self.player play];
}
//暫停音樂,暫停後再開始從暫停的地方開始
- (IBAction)pause {
[self.player pause];
}
//中止音樂,中止後再開始從頭開始
- (IBAction)stop {
[self.player stop];
//這裏要置空
self.player = nil;
}
@end
複製代碼
AudioServicesCreateSystemSoundID(url, &_soundID);
便可,這樣代價最小。#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@property (nonatomic,assign)SystemSoundID soundID;
@end
@implementation ViewController
-(SystemSoundID)soundID{
if (_soundID == 0) {
//生成soundID
CFURLRef url = (__bridge CFURLRef)[[NSBundle mainBundle]URLForResource:@"buyao.wav" withExtension:nil];
AudioServicesCreateSystemSoundID(url, &_soundID);
}
return _soundID;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//播放音效
AudioServicesPlaySystemSound(self.soundID);//不帶震動效果
//AudioServicesPlayAlertSound(<#SystemSoundID inSystemSoundID#>)//帶震動效果
}
@end
複製代碼
使用AVPlayer既能夠播放本地音樂也能夠播放遠程(網絡上的)音樂
播放音頻流的OC代碼以下:
@interface ViewController ()
@property (nonatomic,strong)AVPlayer *player;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//播放音樂
[self.player play];
}
#pragma mark - 懶加載
-(AVPlayer *)player{
if (_player == nil) {
//想要播放遠程音樂,只要把url換成網絡音樂就能夠了
//NSURL *url = [NSURL URLWithString:@"http://cc.stream.qqmusic.qq.com/C100003j8IiV1X8Oaw.m4a?fromtag=52"];
//1.本地的音樂資源
NSURL *url = [[NSBundle mainBundle]URLForResource:@"235319.mp3" withExtension:nil];
//2.這種方法設置的url不能夠動態的切換
_player = [AVPlayer playerWithURL:url];
//2.0建立一個playerItem,能夠經過改變playerItem來進行切歌
//AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
//2.1這種方法能夠動態的換掉url
//_player = [AVPlayer playerWithPlayerItem:playerItem];
//AVPlayerItem *nextItem = [AVPlayerItem playerItemWithURL:nil];
//經過replaceCurrentItemWithPlayerItem:方法來換掉url,進行切歌
//[self.player replaceCurrentItemWithPlayerItem:nextItem];
}
return _player;
}
@end
複製代碼
//初始化音頻播放,返回音頻時長
//播放器相關
var playerItem:AVPlayerItem!
var audioPlayer:AVPlayer!
var audioUrl:String = "" {
didSet{
self.setupPlayerItem()
}
} // 音頻url
func initPlay() {
//初始化播放器
audioPlayer = AVPlayer()
//監聽音頻播放結束
NotificationCenter.default.addObserver(self, selector: #selector(playItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: AudioRecordManager.shared().playerItem)
}
//設置資源
private func setupPlayerItem() {
guard let url = URL(string: audioUrl) else {
return
}
self.playerItem = AVPlayerItem(url: url)
self.audioPlayer.replaceCurrentItem(with: playerItem)
}
//獲取音頻時長
func getDuration() -> Float64 {
if AudioRecordManager.shared().playerItem == nil {
return 0.0
}
let duration : CMTime = playerItem!.asset.duration
let seconds : Float64 = CMTimeGetSeconds(duration)
return seconds
}
func getCurrentTime() -> Float64 {
if AudioRecordManager.shared().playerItem == nil {
return 0.0
}
let duration : CMTime = playerItem!.currentTime()
let seconds : Float64 = CMTimeGetSeconds(duration)
return seconds
}
//播放結束
var audioPlayEndBlock:(()->())?
func playItemDidReachEnd(notifacation:NSNotification) {
audioPlayer?.seek(to: kCMTimeZero)
if let block = audioPlayEndBlock {
block()
}
}
//播放
func playAudio() {
if audioPlayer != nil {
audioPlayer?.play()
}
}
//暫停
var audioStopBlock:(()->())?
func stopAudio() {
if audioPlayer != nil {
audioPlayer?.pause()
if let block = audioStopBlock {
block()
}
}
}
//銷燬
func destroyPlayer() {
if AudioRecordManager.shared().playerItem != nil {
AudioRecordManager.shared().audioPlayer?.pause()
AudioRecordManager.shared().playerItem?.cancelPendingSeeks()
AudioRecordManager.shared().playerItem?.asset.cancelLoading()
}
}
複製代碼
class AVAudioRecorder : NSObject 複製代碼
- 持續錄音,直到用戶中止
- 指定的持續時間的錄音
- 暫停並繼續錄音
- 獲取可用於提供電平測量的輸入聲級數據
在iOS系統中,錄製的音頻來自用戶內置麥克風或耳機麥克風鏈接的設備。在macOS中,音頻來自系統的默認音頻輸入設備,由用戶在系統首選項中設置。
您能夠爲音頻記錄器實現一個委託對象,以響應音頻中斷和音頻解碼錯誤,並完成錄製。
要配置錄音,包括諸如位深度、比特率和採樣率轉換質量等選項,請配置音頻記錄器的設置字典。使用設置中描述的設置鍵。
var settings: [String : Any] { get }
複製代碼
//指示錄音機是否正在錄音的布爾值。
var isRecording: Bool
//與錄音機關聯的音頻文件的URL。
var url: URL
//與記錄器相關聯的AVAudioSessionChannelDescription對象的數組。
var channelAssignments: [AVAudioSessionChannelDescription]?
//時間,以秒爲單位,從錄音開始算起。
var currentTime: TimeInterval
//音頻記錄器所在的主機設備的時間(以秒爲單位)。
var deviceCurrentTime: TimeInterval
//緩衝區中音頻的格式。
var format: AVAudioFormat
複製代碼
音頻會話在應用程序和操做系統之間扮演者中間人的角色。它提供了一種簡單實用的方法是OS得知應用程序應該如何與IOS音頻環境進行交互。你不須要了解與音頻硬件交互的細節,只須要對應用程序的行爲語義上的描述便可。這一點使得你能夠指明應用程序的通常音頻行爲,並能夠把對該行爲的管理委託給音頻會話,這樣OS系統就能夠對用戶使用音頻的體驗進行最適當的管理。
- 激活了音頻播放,可是音頻錄製未激活。
- 當用戶切換響鈴/靜音開發到靜音模式是,應用程序播放的全部音頻都會消失。
- 當設備顯示解鎖屏幕時,全部後臺播放的音頻都會處於靜音狀態。
- 當應用程序播放音頻時,全部後臺播放的音頻都會處於靜音狀態。
class AVAudioSession : NSObject 複製代碼
- 它支持音頻回放,但不容許音頻錄製(tvOS不支持音頻錄製)。
- 在iOS系統中,將鈴聲/靜音開關設置爲靜音模式,應用程序播放的任何音頻都會被靜音。
- 在iOS系統中,鎖定設備會使應用程序的音頻靜音。
- 當應用程序播放音頻時,它會靜音任何其餘背景音頻。
類別 | 來電靜音/鎖屏靜音 | 中斷非混合應用程序的音頻 | 容許音頻輸入(錄製)和輸出(回放) | 做用 |
---|---|---|---|---|
AVAudioSessionCategoryAmbient | Yes | No | Output only | 遊戲,效率應用程序 |
AVAudioSessionCategorySoloAmbient (默認) | Yes | Yes | Output only | 遊戲,效率應用程序 |
AVAudioSessionCategoryPlayback | No | Yes by default; no by using override switch | Output only | 音頻和視頻播放器 |
AVAudioSessionCategoryRecord | No (鎖屏後繼續錄音) | Yes | Input only | 錄音機,音頻捕捉 |
AVAudioSessionCategoryPlayAndRecord | No | Yes by default; no by using override switch | Input and output | VoIP,語音聊天 |
AVAudioSessionCategoryMultiRoute | No | Yes | Input and output | 使用外部的高級A/V應用程序 |
注意:當鈴聲/靜音開關設置爲靜音並鎖定屏幕時,爲了讓你的應用程序繼續播放音頻,請確保UIBackgroundModes音頻鍵已添加到你的應用程序的信息中。plist文件。這個要求是除了你使用正確的類別。
模式標識符 | 兼容的類別 | 做用 |
---|---|---|
AVAudioSessionModeDefault | All | 默認音頻會話模式 |
AVAudioSessionModeMoviePlayback | AVAudioSessionCategoryPlayback | 若是您的應用正在播放電影內容,請指定此模式 |
AVAudioSessionModeVideoRecording | AVAudioSessionCategoryPlayAndRecord,AVAudioSessionCategoryRecord | 若是應用正在錄製電影,則選此模式 |
AVAudioSessionModeVoiceChat | AVAudioSessionCategoryPlayAndRecord | 若是應用須要執行例如 VoIP 類型的雙向語音通訊則選擇此模式 |
AVAudioSessionModeGameChat | AVAudioSessionCategoryPlayAndRecord | 該模式由Game Kit 提供給使用 Game Kit 的語音聊天服務的應用程序設置 |
AVAudioSessionModeVideoChat | AVAudioSessionCategoryPlayAndRecord | 若是應用正在進行在線視頻會議,請指定此模式 |
AVAudioSessionModeSpokenAudio | AVAudioSessionCategoryPlayback | 當須要持續播放語音,同時但願在其餘程序播放短語音時暫停播放此應用語音,選取此模式 |
AVAudioSessionModeMeasurement | AVAudioSessionCategoryPlayAndRecord,AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayback | 若是您的應用正在執行音頻輸入或輸出的測量,請指定此模式 |
AVAudioSessionCategoryPlayAndRecord
分類來配置會話。AVAudioSession *session = [AVAudioSession sharedInstance];
NSError *error;
if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
NSLog(@"Category Error: %@", [error localizedDescription]);
}
if (![session setActive:YES error:&error]) {
NSLog(@"Activation Error: %@", [error localizedDescription]);
}
複製代碼
- AVFormatIDKey 鍵對應寫入內容的音頻格式,它有如下可選值: kAudioFormatLinearPCM kAudioFormatMPEG4AAC kAudioFormatAppleLossless kAudioFormatAppleIMA4 kAudioFormatiLBC kAudioFormatULaw
- kAudioFormatLinearPCM 會將未壓縮的音頻流寫入文件,文件體積大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的壓縮格式會顯著縮小文件,並保證高質量音頻內容。可是要注意,制定的音頻格式與文件類型應該兼容,例如 wav 格式對應 kAudioFormatLinearPCM 值。
AVSampleRateKey
指示採樣率,即對輸入的模擬音頻信號每一秒內的採樣數。經常使用值 8000,16000,22050,44100。 在錄製音頻的質量及最終文件大小方面,採樣率扮演着相當重要的角色。使用低採樣率,好比8kHz,會致使粗粒度,AM廣播類型的錄製效果,不過文件會比較小;使用44.1kHz的採樣率(CD質量的採樣率)會獲得很是高質量的你日日,不過文件就比較大。對於使用什麼採樣率最好沒有一個明確的定義,不過開發者應該儘可能使用標準的採樣率,好比8000,16000,22050,44100。最終是咱們的耳朵在進行判斷。
AVNumberOfChannelsKey
指示定義記錄音頻內容的通道數,指定默認值1意味着使用單聲道錄製,設置2意味着使用立體聲錄製。除非使用外部硬件錄製,不然一般選擇單聲道(也就是AVNumberOfChannelsKey
=1)。
AVEncoderBitDepthHintKey
指示編碼位元深度,從 8 到 32。
AVEncoderAudioQualityKey
指示音頻質量,可選值有: AVAudioQualityMin, AVAudioQualityLow, AVAudioQualityMedium, AVAudioQualityHigh, AVAudioQualityMax。
- 用於寫入音頻的本地文件 URL
- 用於配置錄音會話鍵值信息的字典
- 用於捕捉錯誤的 NSError
NSString *tmpDir = NSTemporaryDirectory();
NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
NSDictionary *settings = @{
AVFormatIDKey : @(kAudioFormatAppleIMA4),
AVSampleRateKey : @44100.0f,
AVNumberOfChannelsKey : @1,
AVEncoderBitDepthHintKey : @16,
AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
};
NSError *error;
self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
if (self.recorder) {
self.recorder.delegate = self;
self.recorder.meteringEnabled = YES;
[self.recorder prepareToRecord];
} else {
NSLog(@"Error: %@", [error localizedDescription]);
}
複製代碼
上面代碼咱們記錄到tmp目錄中的一個名爲memo.cat的文件,在錄製音頻過程當中,.caf (Core Audio Format)格式一般是最好的容器格式,由於它和內容無關而且能夠保持Core Audio支持的任何音頻格式。
此外咱們須要定義錄音設置,以便適應Apple IMA4做爲音頻格式,採樣率44.1kHz,位深度16位,單聲道錄製。這些設置考慮了質量和文件大小的平衡。
@interface ViewController ()
@property (nonatomic,strong) AVAudioRecorder *recorder;
@end
@implementation ViewController
//懶加載
-(AVAudioRecorder *)recorder{
if (_recorder == nil) {
//1.建立沙盒路徑
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
//2.拼接音頻文件
NSString *filePath = [path stringByAppendingPathComponent:@"123.caf"];
//3.轉換成url file://
NSURL *url = [NSURL fileURLWithPath:filePath];
//4.設置錄音的參數
NSDictionary *settings = @{
/**錄音的質量,通常給LOW就能夠了 typedef NS_ENUM(NSInteger, AVAudioQuality) { AVAudioQualityMin = 0, AVAudioQualityLow = 0x20, AVAudioQualityMedium = 0x40, AVAudioQualityHigh = 0x60, AVAudioQualityMax = 0x7F };*/
AVEncoderAudioQualityKey : [NSNumber numberWithInteger:AVAudioQualityLow],
AVEncoderBitRateKey : [NSNumber numberWithInteger:16],
AVSampleRateKey : [NSNumber numberWithFloat:8000],
AVNumberOfChannelsKey : [NSNumber numberWithInteger:2]
};
NSLog(@"%@",url);
//第一個參數就是你要把錄音保存到哪的url
//第二個參數是一些錄音的參數
//第三個參數是錯誤信息
self.recorder = [[AVAudioRecorder alloc]initWithURL:url settings:settings error:nil];
}
return _recorder;
}
//開始錄音
- (IBAction)start:(id)sender {
[self.recorder record];
}
//中止錄音
- (IBAction)stop:(id)sender {
[self.recorder stop];
}
@end
複製代碼
var recorder: AVAudioRecorder?
var player: AVAudioPlayer?
let file_path = PATH_OF_CACHE.appending("/record.wav")
var mp3file_path = PATH_OF_CACHE.appending("/audio.mp3")
private static var _sharedInstance: AudioRecordManager?
private override init() { } // 私有化init方法
/// 單例
///
/// - Returns: 單例對象
class func shared() -> AudioRecordManager {
guard let instance = _sharedInstance else {
_sharedInstance = AudioRecordManager()
return _sharedInstance!
}
return instance
}
/// 銷燬單例
class func destroy() {
_sharedInstance = nil
}
//開始錄音
func beginRecord() {
let session = AVAudioSession.sharedInstance()
//設置session類型
do {
try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
} catch let err{
Dprint("設置類型失敗:\(err.localizedDescription)")
}
//設置session動做
do {
try session.setActive(true)
} catch let err {
Dprint("初始化動做失敗:\(err.localizedDescription)")
}
//錄音設置,注意,後面須要轉換成NSNumber,若是不轉換,你會發現,沒法錄製音頻文件,我猜想是由於底層仍是用OC寫的緣由
let recordSetting: [String: Any] = [AVSampleRateKey: NSNumber(value: 44100.0),//採樣率
AVFormatIDKey: NSNumber(value: kAudioFormatLinearPCM),//音頻格式
AVLinearPCMBitDepthKey: NSNumber(value: 16),//採樣位數
AVNumberOfChannelsKey: NSNumber(value: 2),//通道數
AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)//錄音質量
];
//開始錄音
do {
let url = URL(fileURLWithPath: file_path)
recorder = try AVAudioRecorder(url: url, settings: recordSetting)
recorder!.prepareToRecord()
recorder!.record()
Dprint("開始錄音")
} catch let err {
Dprint("錄音失敗:\(err.localizedDescription)")
}
}
var stopRecordBlock:((_ audioPath:String,_ audioFormat:String)->())?
//結束錄音
func stopRecord() {
let session = AVAudioSession.sharedInstance()
//設置session類型
do {
try session.setCategory(AVAudioSessionCategoryPlayback)
} catch let err{
Dprint("設置類型失敗:\(err.localizedDescription)")
}
//設置session動做
do {
try session.setActive(true)
} catch let err {
Dprint("初始化動做失敗:\(err.localizedDescription)")
}
if let recorder = self.recorder {
if recorder.isRecording {
Dprint("正在錄音,立刻結束它,文件保存到了:\(file_path)")
let manager = FileManager.default
if manager.fileExists(atPath: mp3file_path) {
do {
try manager.removeItem(atPath: mp3file_path)
} catch let err {
Dprint(err)
}
}
AudioWrapper.audioPCMtoMP3(file_path, andPath: mp3file_path)
Dprint("正在錄音,立刻結束它,文件保存到了:\(mp3file_path)")
if let block = stopRecordBlock {
block("/audio.mp3","mp3")
}
}else {
Dprint("沒有錄音,可是依然結束它")
}
recorder.stop()
self.recorder = nil
}else {
Dprint("沒有初始化")
}
}
//取消錄製
func cancelRecord() {
if let recorder = self.recorder {
if recorder.isRecording {
recorder.stop()
self.recorder = nil
}
}
}
///初始化
func initLocalPlay() {
do {
Dprint(mp3file_path)
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: mp3file_path))
player?.delegate = self
Dprint("歌曲長度:\(player!.duration)")
} catch let err {
Dprint("播放失敗:\(err.localizedDescription)")
}
}
//播放本地音頻文件
func play() {
player?.play()
}
//暫停本地音頻
func stop() {
player?.pause()
}
var localPlayFinishBlock:(()->())?
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if let block = AudioRecordManager.shared().localPlayFinishBlock {
block()
}
}
//進度條相關
func progress()->Double{
return (player?.currentTime)!/(player?.duration)!
}
複製代碼
- (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */
- (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
複製代碼
@implementation THMeterTable {
float _scaleFactor;
NSMutableArray *_meterTable;
}
- (id)init {
self = [super init];
if (self) {
float dbResolution = MIN_DB / (TABLE_SIZE - 1);
_meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
_scaleFactor = 1.0f / dbResolution;
float minAmp = dbToAmp(MIN_DB);
float ampRange = 1.0 - minAmp;
float invAmpRange = 1.0 / ampRange;
for (int i = 0; i < TABLE_SIZE; i++) {
float decibels = i * dbResolution;
float amp = dbToAmp(decibels);
float adjAmp = (amp - minAmp) * invAmpRange;
_meterTable[i] = @(adjAmp);
}
}
return self;
}
float dbToAmp(float dB) {
return powf(10.0f, 0.05f * dB);
}
- (float)valueForPower:(float)power {
if (power < MIN_DB) {
return 0.0f;
} else if (power >= 0.0f) {
return 1.0f;
} else {
int index = (int) (power * _scaleFactor);
return [_meterTable[index] floatValue];
}
}
@end
複製代碼
上面代碼建立了一個內部數組,用於保存從計算前的分貝數到使用必定級別分貝解析以後的轉換結果。這裏使用的解析率爲-0.2dB.解析等級經過修改MIN_DB和TABLE_SIZE值進行調整。
每一個分貝值都經過調用dbToAmp函數轉換爲線性範圍內的值,使其處於範圍0(-60dB)到1之間,以後獲得一條有這些範圍內的值構成的平滑曲線,開平方計算並保持到內部查找表格中。這些值在以後須要時均可以經過調用valueForPower
方法來獲取。
- (THLevelPair *)levels {
[self.recorder updateMeters];
float avgPower = [self.recorder averagePowerForChannel:0];
float peakPower = [self.recorder peakPowerForChannel:0];
float linearLevel = [self.meterTable valueForPower:avgPower];
float linearPeak = [self.meterTable valueForPower:peakPower];
return [THLevelPair levelsWithLevel:linearLevel peakLevel:linearPeak];
}
複製代碼
上面代碼首先調用錄音器的updateMeters
方法。該方法必定要正好在讀取當前等級值以前調用,以保證讀取的級別是最新的。以後向通道0請求平均值和峯值。通道都是0索引的,因爲咱們使用單聲道錄製,只須要詢問第一個聲道便可。以後在計量表格中查詢線性聲音強度值並最終建立一個新的THLevelPair實例。
NSTimer
,可是因爲這裏會比較頻繁更新用於展現的計量值以保持動畫效果比較平滑,因此咱們推薦使用CADisplayLink
來更新。CADisplayLink
和NSTimer
相似,不過它能夠與顯示刷新率自動同步。參考書籍:《AV Foundation開發祕籍》,《音視頻開發進階指南基於Android與iOS平臺的實踐》