IOS音視頻(三)AVFoundation 播放和錄音

  • 回顧一下凰盔,上一篇博客“IOS音視頻(二)AVFoundation視頻捕捉” 中講解了關(guān)于AVFoundation框架對攝像頭視頻的捕捉能力,并用兩個demo(一個OC的Demo,一個Swift的Demo)詳細(xì)講解了AVFoundation處理攝像頭視頻捕捉的能力,可以捕捉靜態(tài)圖片枚粘,也可以捕捉實時視頻流,可以錄制視頻飘蚯,還提供了接口操作閃光燈馍迄,開啟手電筒模式等等功能。但是這些講解都是基于蘋果官方文檔一些接口講解的局骤,學(xué)習(xí)了這些我們雖然知道了怎么調(diào)用蘋果的接口實現(xiàn)相關(guān)功能攀圈,但是我們并不知道其中的原理性知識,后續(xù)的博客中將從視頻采集峦甩,視頻編碼赘来,視頻解碼等原理方面詳細(xì)講解现喳,由于時間問題,寫完一篇博客基本上要花費一天的時間犬辰,所以進(jìn)度有些慢嗦篱,博客中也參考了很多大神的博客,但是這些博客是我們平時收集在印象筆記中的幌缝,可能有時候忘記添加原始地址了灸促,后續(xù)有時間會補上。

  • 感覺說了好多廢話涵卵,好了浴栽,先簡單介紹一下:本篇博客主要講解AVFoundation在音頻處理方面的能力。

  • 在音頻方面轿偎,我們主要是指錄制音頻和播放音頻兩個重要的能力吃度,在AVFoundation框架中,等為我們提供了相關(guān)類贴硫,很容易就實現(xiàn)這些功能椿每。但是我們需要理解一下原理性的知識,便于我們開發(fā)中遇到問題就可以及時解決英遭。

  • 在開始講解錄音和播放音頻之前间护,有必要學(xué)習(xí)一下音頻的一些理論知識,方便我們更好的理解挖诸。

  • 本篇博客的錄音Demo點擊這里下載:AVFoundation錄音Demo swift版本, AVFoundation錄音播放demo OC版本

1. 音頻理論知識

1.1 聲音的物理性質(zhì)

  • 聲音是波

聲音是如何產(chǎn)生的呢汁尺?

  1. 聲音是有物體振動而產(chǎn)生的。


    振動產(chǎn)生聲音

    如圖所示多律,當(dāng)小球撞擊到音叉的時候痴突,音叉會產(chǎn)生振動,對周圍的空氣產(chǎn)生擠壓狼荞,從而產(chǎn)生聲音辽装。聲音是一種壓力波,當(dāng)演奏樂器相味、拍打一扇門或者敲擊桌面時拾积,它們的振動都會引起空氣有節(jié)奏的振動,使周圍的空氣產(chǎn)生疏密變化丰涉,形成疏密相間的縱波(可以理解為石頭落入水中激起的波紋)拓巧,由此就產(chǎn)生了聲波,這種現(xiàn)象會一直延續(xù)到振動消失為止一死。

  • 聲波的三要素:
  1. 聲波的三要素是頻率肛度、振幅和波形,頻率代表音階的高低投慈,振幅代表響度承耿,波形代表音色策吠。
  2. 頻率(過零率)越高,波長就越短瘩绒。低頻聲響的波長則較長猴抹,所以其可以更容易地繞過障礙物,因此能量衰減就小锁荔,聲音就會傳得遠(yuǎn)蟀给,反之則會得到完全相反的結(jié)論。
  3. 響度其實就是能量大小的反映阳堕,用不同的力度敲擊桌子跋理,聲音的大小勢必也會不同。在生活中恬总,分貝常用于描述響度的大小前普。聲音超過一定的分貝,人類的耳朵就會受不了壹堰。

人類耳朵的聽力有一個頻率范圍拭卿,大約是20Hz~20kHz,不過贱纠,即使是在這個頻率范圍內(nèi)峻厚,不同的頻率,聽力的感覺也會不一樣谆焊,業(yè)界非常著名的等響曲線惠桃,就是用來描述等響條件下聲壓級與聲波頻率關(guān)系的,人耳對3~4kHz頻率范圍內(nèi)的聲音比較敏感辖试,而對于較低或較高頻率的聲音辜王,敏感度就會有所減弱;在聲壓級較低時罐孝,聽覺的頻率特性會很不均勻呐馆;而在聲壓級較高時,聽覺的頻率特性會變得較為均勻肾档。頻率范圍較寬的音樂摹恰,其聲壓以80~90dB為最佳辫继,超過90dB將會損害人耳(105dB為人耳極限)怒见。

  • 聲音的傳播介質(zhì)

吉他是通過演奏者撥動琴弦來發(fā)出聲音的,鼓是通過鼓槌敲擊鼓面發(fā)出聲音的姑宽,這些聲音的產(chǎn)生都離不開振動遣耍,就連我們說話也是因為聲帶振動而產(chǎn)生聲音的。既然都是振動產(chǎn)生的聲音炮车,那為什么吉他舵变、鼓和人聲聽起來相差這么大呢酣溃?這是因為介質(zhì)不同。我們的聲帶振動發(fā)出聲音之后纪隙,經(jīng)過口腔赊豌、顱腔等局部區(qū)域的反射,再經(jīng)過空氣傳播到別人的耳朵里绵咱,這就是我們說的話被別人聽到的過程,其中包括了最初的發(fā)聲介質(zhì)與顱腔、口腔容客,還有中間的傳播介質(zhì)等翰意。事實上,聲音的傳播介質(zhì)很廣麸锉,它可以通過空氣钠绍、液體和固體進(jìn)行傳播;而且介質(zhì)不同花沉,傳播的速度也不同柳爽,比如,聲音在空氣中的傳播速度為340m/s碱屁,在蒸餾水中的傳播速度為1497m/s泻拦,而在鐵棒中的傳播速度則可以高達(dá)5200m/s;不過忽媒,聲音在真空中是無法傳播的争拐。

  • 吸音和隔音原理
  1. 吸音主要是解決聲音反射而產(chǎn)生的嘈雜感,吸音材料可以衰減入射音源的反射能量晦雨,從而達(dá)到對原有聲源的保真效果架曹,比如錄音棚里面的墻壁上就會使用吸音棉材料。
  2. 隔音主要是解決聲音的透射而降低主體空間內(nèi)的吵鬧感闹瞧,隔音棉材料可以衰減入射音源的透射能量绑雄,從而達(dá)到主體空間的安靜狀態(tài),比如KTV里面的墻壁上就會安裝隔音棉材料奥邮。
  • 回音

當(dāng)我們在高山或空曠地帶高聲大喊的時候万牺,經(jīng)常會聽到回聲(echo)。之所以會有回聲是因為聲音在傳播過程中遇到障礙物會反彈回來洽腺,再次被我們聽到脚粟。但是,若兩種聲音傳到我們的耳朵里的時差小于80毫秒蘸朋,我們就無法區(qū)分開這兩種聲音了核无,其實在日常生活中,人耳也在收集回聲藕坯,只不過由于嘈雜的外界環(huán)境以及回聲的分貝(衡量聲音能量值大小的單位)比較低团南,所以我們的耳朵分辨不出這樣的聲音噪沙,或者說是大腦能接收到但分辨不出。

  • 共鳴

自然界中有光能吐根、水能正歼,生活中有機(jī)械能、電能拷橘,其實聲音也可以產(chǎn)生能量朋腋,例如兩個頻率相同的物體,敲擊其中一個物體時另一個物體也會振動發(fā)聲膜楷。這種現(xiàn)象稱為共鳴旭咽,共鳴證明了聲音傳播可以帶動另一個物體振動,也就是說赌厅,聲音的傳播過程也是一種能量的傳播過程穷绵。

1.2 數(shù)字音頻

1.2.1 采樣、量化和編碼

  • 為了將模擬信號數(shù)字化特愿,需要3個過程分別是采樣仲墨、量化和編碼。

  • 首先要對模擬信號進(jìn)行采樣揍障,所謂采樣就是在時間軸上對信號進(jìn)行數(shù)字化目养。根據(jù)奈奎斯特定理(也稱為采樣定理),按比聲音最高頻率高2倍以上的頻率對聲音進(jìn)行采樣(也稱為AD轉(zhuǎn)換)毒嫡。

  • 對于高質(zhì)量的音頻信號癌蚁,其頻率范圍(人耳能夠聽到的頻率范圍)是20Hz~20kHz,所以采樣頻率一般為44.1kHz兜畸,這樣就可以保證采樣聲音達(dá)到20kHz也能被數(shù)字化努释,從而使得經(jīng)過數(shù)字化處理之后,人耳聽到的聲音質(zhì)量不會被降低咬摇。而所謂的44.1kHz就是代表1秒會采樣44100次伐蒂。

  • 那么,具體的每個采樣又該如何表示呢肛鹏?

  • 這就是量化逸邦。

量化是指在幅度軸上對信號進(jìn)行數(shù)字化,比如用16比特的二進(jìn)制信號來表示聲音的一個采樣在扰,而16比特(一個short)所表示的范圍是[-32768缕减,32767],共有65536個可能取值健田,因此最終模擬的音頻信號在幅度上也分為了65536層烛卧。如下圖所示:


量化采樣過程
  • 既然每一個量化都是一個采樣,那么這么多的采樣該如何進(jìn)行存儲呢妓局?

  • 這就需要-編碼总放。所謂編碼,就是按照一定的格式記錄采樣和量化后的數(shù)字?jǐn)?shù)據(jù)好爬,比如順序存儲或壓縮存儲局雄,等等。

  • 這里面涉及了很多種格式存炮,通常所說的音頻的裸數(shù)據(jù)格式就是脈沖編碼調(diào)制(Pulse Code Modulation炬搭,PCM)數(shù)據(jù)。描述一段PCM數(shù)據(jù)一般需要以下幾個概念:量化格式(sampleFormat)穆桂、采樣率(sampleRate)宫盔、聲道數(shù)(channel)。以CD的音質(zhì)為例:量化格式(有的地方描述為位深度)為16比特(2字節(jié))享完,采樣率為44100灼芭,聲道數(shù)為2,這些信息就描述了CD的音質(zhì)般又。

  • 而對于聲音格式彼绷,還有一個概念用來描述它的大小,稱為數(shù)據(jù)比特率茴迁,即1秒時間內(nèi)的比特數(shù)目寄悯,它用于衡量音頻數(shù)據(jù)單位時間內(nèi)的容量大小。

  • 而對于CD音質(zhì)的數(shù)據(jù)堕义,比特率為多少呢猜旬?

計算如下:
44100 * 16 * 2 = […]

  • 那么在1分鐘里,這類CD音質(zhì)的數(shù)據(jù)需要占據(jù)多大的存儲空間呢倦卖?
  1. 計算如下:
    1378.125 * 60 / 8 / 1024 = 10.09MB
  2. 如果sampleFormat更加精確(比如用4字節(jié)來描述一個采樣)昔馋,或者sampleRate更加密集(比如48kHz的采樣率),那么所占的存儲空間就會更大糖耸,同時能夠描述的聲音細(xì)節(jié)就會越精確秘遏。存儲的這段二進(jìn)制數(shù)據(jù)即表示將模擬信號轉(zhuǎn)換為數(shù)字信號了,以后就可以對這段二進(jìn)制數(shù)據(jù)進(jìn)行存儲嘉竟、播放邦危、復(fù)制,或者進(jìn)行其他任何操作舍扰。
  • 肯定有小伙伴有疑問倦蚪,麥克風(fēng)是如何采集聲音的呢?

麥克風(fēng)里面有一層碳膜边苹,非常薄而且十分敏感陵且。前面介紹過,聲音其實是一種縱波,會壓縮空氣也會壓縮這層碳膜慕购,碳膜在受到擠壓時也會發(fā)出振動聊疲,在碳膜的下方就是一個電極,碳膜在振動的時候會接觸電極沪悲,接觸時間的長短和頻率與聲波的振動幅度和頻率有關(guān)获洲,這樣就完成了聲音信號到電信號的轉(zhuǎn)換。之后再經(jīng)過放大電路處理殿如,就可以實施后面的采樣量化處理了贡珊。


麥克風(fēng)內(nèi)部視圖
  • 那么什么是分貝呢?

分貝是用來表示聲音強度的單位涉馁。日常生活中聽到的聲音门岔,若以聲壓值來表示,由于其變化范圍非常大烤送,可以達(dá)到六個數(shù)量級以上寒随,同時由于我們的耳朵對聲音信號強弱刺激的反應(yīng)不是線性的,而是呈對數(shù)比例關(guān)系胯努,所以引入分貝的概念來表達(dá)聲學(xué)量值牢裳。所謂分貝是指兩個相同的物理量(例如,A1和A0)之比取以10為底的對數(shù)并乘以10(或20)叶沛,即:N= 10 * lg(A1 / A0)
分貝符號為“dB”蒲讯,它是無量綱的。式中A0是基準(zhǔn)量(或參考量)灰署,A1是被量度量判帮。

1.2.2 音頻編碼

  • 前面提到了CD音質(zhì)的數(shù)據(jù)采樣格式,曾計算出每分鐘需要的存儲空間約為10.1MB溉箕,如果僅僅是將其存放在存儲設(shè)備(光盤晦墙、硬盤)中,可能是可以接受的肴茄,但是若要在網(wǎng)絡(luò)中實時在線傳播的話晌畅,那么這個數(shù)據(jù)量可能就太大了,所以必須對其進(jìn)行壓縮編碼寡痰。
  • 壓縮編碼的基本指標(biāo)之一就是壓縮比抗楔,壓縮比通常小于1(否則就沒有必要去做壓縮,因為壓縮就是要減小數(shù)據(jù)容量)拦坠。
  • 壓縮算法包括有損壓縮無損壓縮连躏。
  1. 無損壓縮是指解壓后的數(shù)據(jù)可以完全復(fù)原。在常用的壓縮格式中贞滨,用得較多的是有損壓縮入热,
  2. 有損壓縮是指解壓后的數(shù)據(jù)不能完全復(fù)原,會丟失一部分信息,壓縮比越小勺良,丟失的信息就越多绰播,信號還原后的失真就會越大。根據(jù)不同的應(yīng)用場景(包括存儲設(shè)備郑气、傳輸網(wǎng)絡(luò)環(huán)境幅垮、播放設(shè)備等)腰池,可以選用不同的壓縮編碼算法尾组,如PCM、WAV示弓、AAC讳侨、MP3、Ogg等奏属。
  • 壓縮編碼的原理

壓縮編碼的原理:實際上是壓縮掉冗余信號跨跨,冗余信號是指不能被人耳感知到的信號,包含人耳聽覺范圍之外的音頻信號以及被掩蔽掉的音頻信號等囱皿。人耳聽覺范圍之外的音頻信號在前面已經(jīng)提到過勇婴,所以在此不再贅述。而被掩蔽掉的音頻信號則主要是因為人耳的掩蔽效應(yīng)嘱腥,主要表現(xiàn)為頻域掩蔽效應(yīng)與時域掩蔽效應(yīng)耕渴,無論是在時域還是頻域上,被掩蔽掉的聲音信號都被認(rèn)為是冗余信息齿兔,不進(jìn)行編碼處理橱脸。

  • 壓縮編碼有哪些格式呢?

主要有:WAV編碼分苇, MP3編碼添诉, AAC編碼, Ogg編碼医寿。

  • WAV編碼:
  1. PCM(脈沖編碼調(diào)制)是Pulse Code Modulation的縮寫栏赴。前面已經(jīng)介紹過PCM大致的工作流程,而WAV編碼的一種實現(xiàn)(有多種實現(xiàn)方式靖秩,但是都不會進(jìn)行壓縮操作)就是在PCM數(shù)據(jù)格式的前面加上44字節(jié)须眷,分別用來描述PCM的采樣率、聲道數(shù)盆偿、數(shù)據(jù)格式等信息柒爸。
  2. 特點:音質(zhì)非常好,大量軟件都支持事扭。
  3. 適用場合:多媒體開發(fā)的中間文件捎稚、保存音樂和音效素材。
  • MP3編碼:
  1. MP3具有不錯的壓縮比,使用LAME編碼(MP3編碼格式的一種實現(xiàn))的中高碼率的MP3文件今野,聽感上非常接近源WAV文件葡公,當(dāng)然在不同的應(yīng)用場景下,應(yīng)該調(diào)整合適的參數(shù)以達(dá)到最好的效果条霜。
  2. 特點:音質(zhì)在128Kbit/s以上表現(xiàn)還不錯催什,壓縮比比較高,大量軟件和硬件都支持宰睡,兼容性好蒲凶。
  3. 適用場合:高比特率下對兼容性有要求的音樂欣賞。
  • AAC編碼
  1. AAC是新一代的音頻有損壓縮技術(shù)拆内,它通過一些附加的編碼技術(shù)(比如PS旋圆、SBR等),衍生出了LC-AAC麸恍、HE-AAC灵巧、HE-AAC v2三種主要的編碼格式。LC-AAC是比較傳統(tǒng)的AAC抹沪,相對而言刻肄,其主要應(yīng)用于中高碼率場景的編碼(≥80Kbit/s);HE-AAC(相當(dāng)于AAC+SBR)主要應(yīng)用于中低碼率場景的編碼(≤80Kbit/s)融欧;而新近推出的HE-AAC v2(相當(dāng)于AAC+SBR+PS)主要應(yīng)用于低碼率場景的編碼(≤48Kbit/s)敏弃。事實上大部分編碼器都設(shè)置為≤48Kbit/s自動啟用PS技術(shù),而>48Kbit/s則不加PS蹬癌,相當(dāng)于普通的HE-AAC权她。
  2. 特點:在小于128Kbit/s的碼率下表現(xiàn)優(yōu)異,并且多用于視頻中的音頻編碼逝薪。
  3. 適用場合:128Kbit/s以下的音頻編碼隅要,多用于視頻中音頻軌的編碼。
  • Ogg編碼:
  1. Ogg是一種非常有潛力的編碼董济,在各種碼率下都有比較優(yōu)秀的表現(xiàn)步清,尤其是在中低碼率場景下。Ogg除了音質(zhì)好之外虏肾,還是完全免費的廓啊,這為Ogg獲得更多的支持打好了基礎(chǔ)。Ogg有著非常出色的算法封豪,可以用更小的碼率達(dá)到更好的音質(zhì)谴轮,128Kbit/s的Ogg比192Kbit/s甚至更高碼率的MP3還要出色。但目前因為還沒有媒體服務(wù)軟件的支持吹埠,因此基于Ogg的數(shù)字廣播還無法實現(xiàn)第步。Ogg目前受支持的情況還不夠好疮装,無論是軟件上的還是硬件上的支持,都無法和MP3相提并論粘都。
  2. 特點:可以用比MP3更小的碼率實現(xiàn)比MP3更好的音質(zhì)廓推,高中低碼率下均有良好的表現(xiàn),兼容性不夠好翩隧,流媒體特性不支持樊展。
  3. 適用場合:語音聊天的音頻消息場景。

1.3 音頻編解碼

  • 只要是Core Audio框架支持的音頻編解碼堆生, AVFoundation框架都可以支持专缠, 這意味著 AVFoundation能夠支持大量不同格式的資源。然而在不用線性PCM音頻的情況下顽频,更多的只能使用AAC藤肢。

  • 高級音頻編碼AAC是H.264標(biāo)準(zhǔn)相應(yīng)的音頻處理方式太闺,目前已經(jīng)成為音頻流和下載的音頻資源中最主流的編碼方式糯景。這種格式比MP3格式有著顯著的提升,可以在低比特率的前提下提供更高質(zhì)量的音頻省骂,是在Web上發(fā)布和傳播的音頻格式中最為理想的蟀淮。此外,AAC沒有來自證書和許可方面的限制钞澳,這一限制曾經(jīng)在MP3格式上飽受詬病怠惶。

  • AVFoundationCore Audio框架都提供對MP3數(shù)據(jù)解碼支持,但是不支持對其進(jìn)行編碼轧粟。

  • AVFoundation框架最開始是一個僅針對音頻的框架策治,該框架的前身在IOS2.2版本中引入,只包含一個出來音頻播放的類兰吟;在iOS 3.0中通惫,蘋果公司增加了音頻錄制功能。雖然這些類都是目前該框架中最古老的混蔼,但他們?nèi)稳皇亲畛S玫膸讉€類履腋。

  • 上面講解了這么多音頻理論知識,接下來我們將從 AVFoundation的兩個基礎(chǔ)類AVAudioPlayerAVAudioRecorder來講解音頻播放和音頻錄制功能惭嚣。

2. 播放音頻

2.1 AVAudioPlayer簡介

  • IOS中能播放音頻的類有很多遵湖,這里我們主要講解用AVAudioPlayer類來實現(xiàn)音頻播放功能。
  • 這里先來簡單介紹一下AVAudioPlayer
  • AVAudioPlayer類實戰(zhàn)IOS2.2就開始支持的晚吞。它是一種提供從文件或存儲器中播放音頻數(shù)據(jù)的音頻播放器延旧。除非您正在播放從網(wǎng)絡(luò)流中捕獲的音頻或需要非常低的I/O延遲,否則請將該類用于音頻播放槽地。
  • AVAudioPlayer繼承自NSObject
  • AVAudioPlayer提供了以下功能:
  1. 播放任何持續(xù)時間的聲音
  2. 播放來自文件或內(nèi)存緩沖區(qū)的聲音
  3. 循環(huán)播放
  4. 同時播放多個聲音迁沫,每個音頻播放器一個聲音烹卒,精確同步
  5. 控制相對播放級別、立體聲定位和播放速度
  6. 查找聲音文件中的特定點弯洗,該點支持快進(jìn)和快退等應(yīng)用程序特性.
  7. 獲取可用于回放級別測量的數(shù)據(jù).
  • AVAudioPlayer類允許你在iOS和macOS中播放任何音頻格式的聲音旅急。你可以實現(xiàn)一個委托來處理中斷(比如iOS上的來電),并在聲音播放完畢時更新用戶界面牡整。委派方法在AVAudioPlayerDelegate中描述藐吮。該類使用Objective-C聲明的屬性特性來管理關(guān)于聲音的信息,比如聲音時間軸中的播放點逃贝,以及訪問播放選項谣辞,比如音量和循環(huán)。
  • 要在iOS上為回放配置一個適當(dāng)?shù)囊纛l會話沐扳,請參見AVAudioSessionAVAudioSessionDelegate泥从。
  • AVAudioPlayerDelegate主要提供了兩個代理回調(diào)方法:

(1)當(dāng)音頻播放完成時,會調(diào)用下面的回調(diào)方法:

optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, 
                             successfully flag: Bool)

(2)當(dāng)音頻播放器在播放過程中遇到解碼錯誤時會調(diào)用下面這個回調(diào)方法:

optional func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, 
                                       error: Error?)
  • 要播放沪摄、暫停或停止音頻播放器,調(diào)用它的一個回放控制方法可以使用下面這些接口:


//異步播放聲音哄陶。
func play() -> Bool

//以異步方式播放聲音鳍徽,從音頻輸出設(shè)備時間軸中的指定點開始播放胖翰。
func play(atTime: TimeInterval) -> Bool


//暫停播放;聲音準(zhǔn)備好從它停止的地方恢復(fù)播放鹃两。
func pause()

//停止播放并撤消播放所需的設(shè)置。
func stop()

//通過預(yù)加載音頻播放器的緩沖區(qū)來準(zhǔn)備播放茸习。
func prepareToPlay() -> Bool

//淡入到一個新的卷在一個特定的持續(xù)時間。
func setVolume(Float, fadeDuration: TimeInterval)

//一個布爾值察净,指示音頻播放器是否正在播放(真)或不(假)晨缴。
var isPlaying: Bool

//音頻播放器的播放音量阁吝,線性范圍從0.0到1.0。
var volume: Float

//音頻播放器的立體聲平移位置。
var pan: Float

//音頻播放器的播放速率借宵。
var rate: Float

//一個布爾值断部,用于指定是否為音頻播放器啟用播放速率調(diào)整猎贴。
var enableRate: Bool

//一個聲音返回到開始的次數(shù),到達(dá)結(jié)束時蝴光,重復(fù)播放她渴。
var numberOfLoops: Int

//音頻播放器的委托對象。
var delegate: AVAudioPlayerDelegate?

//一種協(xié)議蔑祟,它允許一個委托響應(yīng)音頻中斷和音頻解碼錯誤趁耗,并完成聲音的回放。
protocol AVAudioPlayerDelegate

//音頻播放器的設(shè)置字典疆虚,包含與播放器相關(guān)的聲音信息苛败。
var settings: [String : Any]
  • 此外AVAudioPlayer提供了管理關(guān)于聲音的信息的接口:

//聲音中與音頻播放器相關(guān)聯(lián)的音頻通道的數(shù)量。
var numberOfChannels: Int

//與音頻播放器相關(guān)聯(lián)的AVAudioSessionChannelDescription對象的數(shù)組
var channelAssignments: [AVAudioSessionChannelDescription]?

//與音頻播放器相關(guān)聯(lián)的聲音的總持續(xù)時間(以秒為單位).
var duration: TimeInterval

//播放點径簿,以秒為單位罢屈,在與音頻播放器關(guān)聯(lián)的聲音的時間軸內(nèi)。
var currentTime: TimeInterval

//音頻輸出設(shè)備的時間值牍帚,以秒為單位儡遮。
var deviceCurrentTime: TimeInterval

//與音頻播放器關(guān)聯(lián)的聲音的URL。
var url: URL?

//包含與音頻播放器相關(guān)聯(lián)的聲音的數(shù)據(jù)對象暗赶。
var data: Data?

//當(dāng)前音頻播放器的UID鄙币。
var currentDevice: String?

//緩沖區(qū)中音頻的格式肃叶。
var format: AVAudioFormat
  • 此外AVAudioPlayer還提供了使用音頻電平測量的接口:

//一個布爾值,用于指定音頻播放器的音頻電平測量開/關(guān)狀態(tài)十嘿。
var isMeteringEnabled: Bool

//返回給定頻道的平均功率(以分貝為單位)因惭。
func averagePower(forChannel: Int) -> Float

//返回給定頻道的峰值功率,以分貝表示所播放的聲音绩衷。
func peakPower(forChannel: Int) -> Float

//返回刷新音頻播放器所有頻道的平均和峰值功率值蹦魔。
func updateMeters()

//格式標(biāo)識符咳燕。
let AVFormatIDKey: String

//采樣率勿决,用赫茲表示,表示為NSNumber浮點值招盲。一般為8000低缩,和16K
let AVSampleRateKey: String

//用NSNumber整數(shù)值表示的通道數(shù)。
let AVNumberOfChannelsKey: String

2.2 AVAudioPlayer實現(xiàn)音頻播放

  • AVAudioPlayer 構(gòu)建于 Core Audio 的 C-based Audio Queue Services 最頂層曹货,局限性在于無法從網(wǎng)絡(luò)流播放音頻咆繁,不能訪問原始音頻樣本,不能滿足非常低的時延顶籽。

2.2.1 創(chuàng)建 AVAudioPlayer

  • 可以通過 NSData 或本地音頻文件的 NSURL 兩種方式創(chuàng)建 AVAudioPlayer玩般。
    NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
    self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
    if (self.player) {
        [self.player prepareToPlay];
    }

創(chuàng)建出 AVAudioPlayer 后建議調(diào)用 prepareToPlay 方法,這個方法會取得需要的音頻硬件并預(yù)加載 Audio Queue 的緩沖區(qū)礼饱,當(dāng)然如果不主動調(diào)用坏为,執(zhí)行 play 方法時也會默認(rèn)調(diào)用,但是會造成輕微播放的延時慨仿。

2.2.2 對播放進(jìn)行控制

AVAudioPlayer 的 play 可以播放音頻久脯,stop 和 pause 都可以暫停播放,但是 stop 會撤銷調(diào)用 prepareToPlay 所做的設(shè)置镰吆。從上面介紹的AVAudioPlayer屬性可以知道如何設(shè)置。具體設(shè)置 如下:

  1. 修改播放器的音量:播放器音量獨立于系統(tǒng)音量跑慕,音量或播放增益定義為 0.0(靜音)到 1.0(最大音量)之間的浮點值
  2. 修改播放器的 pan 值:允許使用立體聲播放聲音万皿,pan 值從 -1.0(極左)到 1.0(極右),默認(rèn)值 0.0(居中)
  3. 調(diào)整播放率:0.5(半速)到 2.0(2 倍速)
  4. 設(shè)置 numberOfLoops 實現(xiàn)無縫循環(huán):-1 表示無限循環(huán)(音頻循環(huán)可以是未壓縮的線性 PCM 音頻核行,也可以是 AAC 之類的壓縮格式音頻司顿,MP3 格式不推薦循環(huán))
  5. 音頻計量:當(dāng)播放發(fā)生時從播放器讀取音量力度的平均值和峰值

2.2.3 播放/停止音頻

  • 播放音頻
        NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
        for (AVAudioPlayer *player in self.players) {
            [player playAtTime:delayTime];
        }
        self.playing = YES;

對于多個需要播放的音頻荧嵌,如果希望同步播放效果,則需要捕捉當(dāng)前設(shè)備時間并添加一個小延時,從而具有一個從開始播放時間計算的參照時間慰丛。deviveCurrentTime 是一個獨立于系統(tǒng)事件的音頻設(shè)備的時間值,當(dāng)有多于 audioPlayer 處于 play 或者 pause 狀態(tài)時 deviveCurrentTime 會單調(diào)增加,沒有時置位為 0。playAtTime 的參數(shù) time 要求必須是基于 deviveCurrentTime 且大于等于 deviveCurrentTime 的時間如筛。

  • 停止播放音頻
        for (AVAudioPlayer *player in self.players) {
            [player stop];
            player.currentTime = 0.0f;
        }

暫停時需要將 audioPlayer 的 currentTime 值設(shè)置為 0.0,當(dāng)音頻正在播放時抒抬,這個值用于標(biāo)識當(dāng)前播放位置的偏移杨刨,不播放音頻時標(biāo)識重新播放音頻的起始偏移。

  • 調(diào)用play方法可以實現(xiàn)立即播放音頻的功能擦剑,pause方法可以對播放暫停妖胀,那么可想而知stop方法可以停止播放行為。有趣的是惠勒,pausestop方法在應(yīng)用程序外面看來實現(xiàn)的功能都是停止當(dāng)前播放行為赚抡。下一時間里我們調(diào)用play方法,通過pausestop方法停止的音頻都會繼續(xù)播放纠屋。
  • 這兩者最主要的區(qū)別在底層處理上涂臣。調(diào)用stop方法撤銷調(diào)用prepareToPlay時所做的設(shè)置,而調(diào)用pause方法則不會巾遭。

2.2.4 修改音量肉康、播放速率

  • 我們可以簡單通過設(shè)置AVAudioPlayer對象的屬性來修改音量,播放速率灼舍,是否循環(huán)播放等吼和,如下:
player.enableRate = YES;
player.rate = rate;
player.volume = volume;
player.pan = pan;
player.numberOfLoops = -1;
  • 修改播放器的音量:播放器的音量獨立于系統(tǒng)音量,我們可以通過對播放器音量的處理實現(xiàn)很多有趣的效果骑素,比如聲音漸隱效果炫乓。音量或播放增益定義為0.0(靜音)到 1.0 (最大音量)之間的浮點值。
  • 修改播放器的pan值: 允許使用立體聲播放聲音献丑,播放器的pan值是有一個浮點數(shù)表示末捣,范圍從-1.0(極左)到1.0(極右)。默認(rèn)值為0.0(居中)
  • 調(diào)整播放率: IOS5版本中加入了一個強大功能创橄,即允許用戶在不改變音調(diào)的情況下調(diào)整播放率箩做,范圍從0.5(半速)到2.0(2倍速)。如果正記錄一首復(fù)雜的音樂或語音妥畏,放慢速度會有很大的幫助邦邦;當(dāng)我們想快速瀏覽一份政府常規(guī)會議內(nèi)容是,加速播放就很有幫助醉蚁。
  • 通過設(shè)置 numberOfLoops 屬性實現(xiàn)音頻無縫循環(huán) : 給這個屬性設(shè)置一個大于0 的數(shù)燃辖,可以實現(xiàn)播放器n次循環(huán)播放。相反网棍,為該屬性賦值-1會導(dǎo)致播放器無限循環(huán)黔龟。

2.2.5 配置音頻會話

  • 由于音頻會話是所有應(yīng)用公用的,所有一般在程序啟動時設(shè)置,是通過AVAudioSession單例來設(shè)置的氏身。

  • 如果希望應(yīng)用程序播放音頻時屏蔽靜音切換動作巍棱,需要設(shè)置會話分類為 AVAudioSessionCategoryPlayback,但是如果希望按下鎖屏后還可以播放观谦,就需要在 plist 里加入一個 Required background modes 類型的數(shù)組拉盾,在其中添加 App plays audio or streams audio/video using AirPlay。

  • 后面講解錄音時還會詳細(xì)講解配置音頻會話豁状。

2.2.6 處理中斷事件

  • 中斷事件是指電話呼入捉偏、鬧鐘響起、彈出 FaceTime 等泻红,中斷事件發(fā)生時系統(tǒng)會調(diào)用 AVAudioPlayer 的 AVAudioPlayerDelegate 類型的 delegate 的下列方法:
- (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);

中斷結(jié)束調(diào)用的方法會帶入一個 options 參數(shù)夭禽,如果是 AVAudioSessionInterruptionOptionShouldResume 則表明可以恢復(fù)播放音頻了。

在準(zhǔn)備為出現(xiàn)的中斷時間采取動作前谊路,首先要得到中斷出現(xiàn)的通知讹躯,注冊應(yīng)用程序的AVAudioSession發(fā)送的通知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字典,根據(jù)這個字典可以確定采取哪些適合的操作缠劝。如下代碼:

@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),我們調(diào)用stop方法,并通過調(diào)用委托函數(shù)playbackStopped方法向委托通知中斷狀態(tài)惨恭。很重要的一點是當(dāng)通知被接收是秉馏,音頻會話已經(jīng)被終止,且AVAudioPlayer實例處于暫停狀態(tài)脱羡。調(diào)用控制啟動stop方法只能更新內(nèi)部狀態(tài)萝究,并不能停止播放。

2.2.7 處理線路改變

  • 在 iOS 設(shè)備上添加或移除音頻輸入锉罐、輸出線路時會引發(fā)線路改變帆竹,有多重原因會導(dǎo)致線路的變化,比如用戶插入耳機(jī)或者短褲USB麥克風(fēng)脓规。當(dāng)這些事件發(fā)生時栽连,音頻會根據(jù)這些情況改變輸入或者輸出線路,同時AVFoundation會廣播一個描述該變化的通知給所有相關(guān)偵聽器侨舆。
  • 最佳實踐是升酣,插入耳機(jī)時播放動作不改動,拔出耳機(jī)時應(yīng)當(dāng)暫停播放态罪。
  • 要處理線路改變,我們只能通過系統(tǒng)通知來實現(xiàn)下面。
  • 首先需要監(jiān)聽通知复颈,代碼如下:
        NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
        [nsnc addObserver:self
                 selector:@selector(handleRouteChange:)
                     name:AVAudioSessionRouteChangeNotification
                   object:[AVAudioSession sharedInstance]];
  • 然后判斷是舊設(shè)備不可達(dá)事件,進(jìn)一步取出舊設(shè)備的描述,判斷舊設(shè)備是否是耳機(jī)耗啦,再做暫停播放處理凿菩。代碼如下:
- (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];
        }
    }
}

接收到通知后要做的第一件事情是判斷線路變更發(fā)生的原因。查看保存userinfo字典中的表示原因的AVAudioSessionRouteChangeReasonKey值帜讲。這個返回值是一個用于表示變化原因的無符號整數(shù)衅谷。通過原因可以推斷出不同的事件。比如有新設(shè)備接入或者改變音頻會話類型似将,不過我們需要特殊注意的是耳機(jī)短褲這個事件获黔,這個事件的對應(yīng)原因為:AVAudioSessionRouteChangeReasonOldDeviceUnavailable

知道有設(shè)備斷開連接后,需要向userinfo字典提出請求在验,以獲得其中用于描述前一個線路的AVAudioSessionPortDescription玷氏。線路的描述信息是整合在一個熟人NSArray和一個輸出NSArray中。在上述情況下腋舌,你需要從線路描述中找出第一個輸出接口并判斷其是否為耳機(jī)接口盏触。如果是,則停止播放块饺,并調(diào)用委托函數(shù)的playbackStopeed方法赞辩。

這里 AVAudioSessionPortHeadphones 只包含了有線耳機(jī),無線藍(lán)牙耳機(jī)需要判斷 AVAudioSessionPortBluetoothA2DP 值授艰。

2.2.8 音頻播放處理

2.2.8.1 播放本地音頻

  • 我們可以使用AVAudioPlayer播放本地音樂辨嗽,而播放遠(yuǎn)程音頻需要使用AVPlayer
#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.創(chuàng)建AVAudioPlayer對象
        _player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:nil];
        //3.準(zhǔn)備播放(緩沖,提高播放的流暢性)
        [_player prepareToPlay];
    }
    return _player;
}
//播放(異步播放)
- (IBAction)play {
    [self.player play];
}
//暫停音樂想诅,暫停后再開始從暫停的地方開始
- (IBAction)pause {
    [self.player pause];
}
//停止音樂召庞,停止后再開始從頭開始
- (IBAction)stop {
    [self.player stop];
    //這里要置空
    self.player = nil;
}  
@end
  • 而我們播放本地的短音頻(“短音頻”是指通常在程序中的播放時長為1~2秒)一般直接使用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

2.2.8.2 播放遠(yuǎn)程音頻

  • 使用AVPlayer既可以播放本地音樂也可以播放遠(yuǎn)程(網(wǎng)絡(luò)上的)音樂

  • 播放音頻流的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) {
            
    //想要播放遠(yuǎn)程音樂篮灼,只要把url換成網(wǎng)絡(luò)音樂就可以了
    //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.這種方法設(shè)置的url不可以動態(tài)的切換
    _player = [AVPlayer playerWithURL:url];

    //2.0創(chuàng)建一個playerItem,可以通過改變playerItem來進(jìn)行切歌
    //AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
    //2.1這種方法可以動態(tài)的換掉url
    //_player = [AVPlayer playerWithPlayerItem:playerItem];
    
    //AVPlayerItem *nextItem = [AVPlayerItem playerItemWithURL:nil];
    //通過replaceCurrentItemWithPlayerItem:方法來換掉url徘禁,進(jìn)行切歌
    //[self.player replaceCurrentItemWithPlayerItem:nextItem];
    
    }
    return _player;
}
@end
  • 播放音頻流的Swift代碼如下:
//初始化音頻播放诅诱,返回音頻時長
//播放器相關(guān)
var playerItem:AVPlayerItem!
var audioPlayer:AVPlayer!

var audioUrl:String = "" {
    didSet{
        self.setupPlayerItem()
    }
} // 音頻url

func initPlay() {
    //初始化播放器
    audioPlayer = AVPlayer()
    //監(jiān)聽音頻播放結(jié)束
    NotificationCenter.default.addObserver(self, selector: #selector(playItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: AudioRecordManager.shared().playerItem)
    
}

//設(shè)置資源
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
}

//播放結(jié)束
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()
    }
} 

3. 錄制音頻

3.1 AVAudioRecorder 簡介

  • AVAudioRecorder 是AVFoundation框架提供的在應(yīng)用程序中提供音頻錄制功能的類。它也是直接繼承NSObject在 IOS3.0才提供支持送朱。
class AVAudioRecorder : NSObject
  1. 持續(xù)錄音娘荡,直到用戶停止
  2. 指定的持續(xù)時間的錄音
  3. 暫停并繼續(xù)錄音
  4. 獲取可用于提供電平測量的輸入聲級數(shù)據(jù)
  • 在iOS系統(tǒng)中,錄制的音頻來自用戶內(nèi)置麥克風(fēng)或耳機(jī)麥克風(fēng)連接的設(shè)備驶沼。在macOS中炮沐,音頻來自系統(tǒng)的默認(rèn)音頻輸入設(shè)備,由用戶在系統(tǒng)首選項中設(shè)置回怜。

  • 您可以為音頻記錄器實現(xiàn)一個委托對象大年,以響應(yīng)音頻中斷和音頻解碼錯誤,并完成錄制。

  • 要配置錄音翔试,包括諸如位深度轻要、比特率和采樣率轉(zhuǎn)換質(zhì)量等選項,請配置音頻記錄器的設(shè)置字典垦缅。使用設(shè)置中描述的設(shè)置鍵冲泥。

var settings: [String : Any] { get }
  • 只有在顯式地調(diào)用prepareToRecord()方法或通過開始錄制隱式地調(diào)用它之后,錄音機(jī)設(shè)置才有效壁涎。音頻設(shè)置鍵在音頻設(shè)置和格式中描述凡恍。
  • 管理有關(guān)錄音的信息Setting字典主要有下面這些key:

//指示錄音機(jī)是否正在錄音的布爾值。
var isRecording: Bool

//與錄音機(jī)關(guān)聯(lián)的音頻文件的URL粹庞。
var url: URL

//與記錄器相關(guān)聯(lián)的AVAudioSessionChannelDescription對象的數(shù)組咳焚。
var channelAssignments: [AVAudioSessionChannelDescription]?

//時間,以秒為單位庞溜,從錄音開始算起革半。
var currentTime: TimeInterval

//音頻記錄器所在的主機(jī)設(shè)備的時間(以秒為單位)。
var deviceCurrentTime: TimeInterval

//緩沖區(qū)中音頻的格式流码。
var format: AVAudioFormat

3.2 AVAudioSession 簡介

音頻會話在應(yīng)用程序和操作系統(tǒng)之間扮演者中間人的角色又官。它提供了一種簡單實用的方法是OS得知應(yīng)用程序應(yīng)該如何與IOS音頻環(huán)境進(jìn)行交互。你不需要了解與音頻硬件交互的細(xì)節(jié)漫试,只需要對應(yīng)用程序的行為語義上的描述即可六敬。這一點使得你可以指明應(yīng)用程序的一般音頻行為,并可以把對該行為的管理委托給音頻會話驾荣,這樣OS系統(tǒng)就可以對用戶使用音頻的體驗進(jìn)行最適當(dāng)?shù)墓芾怼?/p>

  • 所有IOS應(yīng)用程序都具有音頻會話外构,無論其是否使用。默認(rèn)音頻會話來自于以下一些預(yù)配置:
  1. 激活了音頻播放播掷,但是音頻錄制未激活审编。
  2. 當(dāng)用戶切換響鈴/靜音開發(fā)到靜音模式是,應(yīng)用程序播放的所有音頻都會消失歧匈。
  3. 當(dāng)設(shè)備顯示解鎖屏幕時垒酬,所有后臺播放的音頻都會處于靜音狀態(tài)。
  4. 當(dāng)應(yīng)用程序播放音頻時件炉,所有后臺播放的音頻都會處于靜音狀態(tài)勘究。
  • 要實現(xiàn)錄音,我們需要配置音頻會話斟冕,因為系統(tǒng)默認(rèn)是Solo Ambient模式口糕,要錄音需要設(shè)置為Play and Record模式。
  • 若要配置適當(dāng)?shù)匿浺魰捒纳撸垍⒖?a target="_blank">AVAudioSession和AVAudioSessionDelegate走净。
  • 要實現(xiàn)錄音功能券时,我們有必要來理解下AVAudioSession類,AVAudioSession類也是直接繼承NSObject
class AVAudioSession : NSObject
  • 音頻會話充當(dāng)應(yīng)用程序和操作系統(tǒng)之間的中介伏伯,進(jìn)而充當(dāng)?shù)讓右纛l硬件之間的中介。您使用一個音頻會話來與操作系統(tǒng)通信應(yīng)用程序音頻的一般性質(zhì)捌袜,而不詳細(xì)說明特定的行為或與音頻硬件所需的交互说搅。您將這些細(xì)節(jié)的管理委托給音頻會話,以確保操作系統(tǒng)能夠最好地管理用戶的音頻體驗虏等。
  • 所有iOS弄唧、tvOS和watchOS應(yīng)用程序都有一個默認(rèn)的音頻會話,并預(yù)先配置了以下行為:
  1. 它支持音頻回放霍衫,但不允許音頻錄制(tvOS不支持音頻錄制)候引。
  2. 在iOS系統(tǒng)中,將鈴聲/靜音開關(guān)設(shè)置為靜音模式敦跌,應(yīng)用程序播放的任何音頻都會被靜音澄干。
  3. 在iOS系統(tǒng)中,鎖定設(shè)備會使應(yīng)用程序的音頻靜音柠傍。
  4. 當(dāng)應(yīng)用程序播放音頻時麸俘,它會靜音任何其他背景音頻。

3.2.1 音頻會話模式

  • 雖然默認(rèn)的音頻會話提供了有用的行為惧笛,但它通常不提供媒體應(yīng)用程序需要的音頻行為从媚。要更改默認(rèn)行為,需要配置應(yīng)用程序的音頻會話類別患整。
  • 你可以使用七種可能的類別(參見音頻會話類別和模式)拜效,但是回放是回放應(yīng)用程序最常用的一種。這個類別表明音頻播放是你的應(yīng)用程序的核心功能各谚。當(dāng)你指定這個類別時紧憾,你的應(yīng)用程序的音頻將繼續(xù)與鈴聲/靜音開關(guān)設(shè)置為靜音模式(只有iOS)。使用這個類別嘲碧,你也可以播放背景音頻稻励,如果你在圖片背景模式中使用音頻,AirPlay和圖片愈涩。有關(guān)更多信息望抽,請參見啟用背景音頻
  • 音頻會話7種類別行為如下:
類別 來電靜音/鎖屏靜音 中斷非混合應(yīng)用程序的音頻 允許音頻輸入(錄制)和輸出(回放) 作用
AVAudioSessionCategoryAmbient Yes No Output only 游戲履婉,效率應(yīng)用程序
AVAudioSessionCategorySoloAmbient (默認(rèn)) Yes Yes Output only 游戲煤篙,效率應(yīng)用程序
AVAudioSessionCategoryPlayback No Yes by default; no by using override switch Output only 音頻和視頻播放器
AVAudioSessionCategoryRecord No (鎖屏后繼續(xù)錄音) Yes Input only 錄音機(jī),音頻捕捉
AVAudioSessionCategoryPlayAndRecord No Yes by default; no by using override switch Input and output VoIP,語音聊天
AVAudioSessionCategoryMultiRoute No Yes Input and output 使用外部的高級A/V應(yīng)用程序

注意:當(dāng)鈴聲/靜音開關(guān)設(shè)置為靜音并鎖定屏幕時毁腿,為了讓你的應(yīng)用程序繼續(xù)播放音頻辑奈,請確保UIBackgroundModes音頻鍵已添加到你的應(yīng)用程序的信息中苛茂。plist文件。這個要求是除了你使用正確的類別鸠窗。

  • 模式及相關(guān)類別:
模式標(biāo)識符 兼容的類別 作用
AVAudioSessionModeDefault All 默認(rèn)音頻會話模式
AVAudioSessionModeMoviePlayback AVAudioSessionCategoryPlayback 如果您的應(yīng)用正在播放電影內(nèi)容妓羊,請指定此模式
AVAudioSessionModeVideoRecording AVAudioSessionCategoryPlayAndRecord,AVAudioSessionCategoryRecord 如果應(yīng)用正在錄制電影稍计,則選此模式
AVAudioSessionModeVoiceChat AVAudioSessionCategoryPlayAndRecord 如果應(yīng)用需要執(zhí)行例如 VoIP 類型的雙向語音通信則選擇此模式
AVAudioSessionModeGameChat AVAudioSessionCategoryPlayAndRecord 該模式由Game Kit 提供給使用 Game Kit 的語音聊天服務(wù)的應(yīng)用程序設(shè)置
AVAudioSessionModeVideoChat AVAudioSessionCategoryPlayAndRecord 如果應(yīng)用正在進(jìn)行在線視頻會議躁绸,請指定此模式
AVAudioSessionModeSpokenAudio AVAudioSessionCategoryPlayback 當(dāng)需要持續(xù)播放語音,同時希望在其他程序播放短語音時暫停播放此應(yīng)用語音臣嚣,選取此模式
AVAudioSessionModeMeasurement AVAudioSessionCategoryPlayAndRecord净刮,AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayback 如果您的應(yīng)用正在執(zhí)行音頻輸入或輸出的測量硅则,請指定此模式

3.2.2 配置音頻會話

3.2 AVAudioRecorder 實現(xiàn)錄音功能

3.2.1 錄音功能細(xì)節(jié)

3.2.1.1 錄音時配置音頻會話模式

  • 上面我們已經(jīng)詳細(xì)講解關(guān)于會話模式配置的應(yīng)用場景淹父,錄音和播放應(yīng)用應(yīng)當(dāng)使用 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]);
    }

3.2.1.2 錄音時通用設(shè)置參數(shù)配置

  • 音頻格式
  1. AVFormatIDKey 鍵對應(yīng)寫入內(nèi)容的音頻格式怎虫,它有以下可選值:
    kAudioFormatLinearPCM
    kAudioFormatMPEG4AAC
    kAudioFormatAppleLossless
    kAudioFormatAppleIMA4
    kAudioFormatiLBC
    kAudioFormatULaw
  2. kAudioFormatLinearPCM 會將未壓縮的音頻流寫入文件暑认,文件體積大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的壓縮格式會顯著縮小文件揪垄,并保證高質(zhì)量音頻內(nèi)容穷吮。但是要注意,制定的音頻格式與文件類型應(yīng)該兼容饥努,例如 wav 格式對應(yīng) kAudioFormatLinearPCM 值捡鱼。
  • 采樣率

AVSampleRateKey 指示采樣率,即對輸入的模擬音頻信號每一秒內(nèi)的采樣數(shù)酷愧。常用值 8000驾诈,16000,22050溶浴,44100乍迄。
在錄制音頻的質(zhì)量及最終文件大小方面,采樣率扮演著至關(guān)重要的角色士败。使用低采樣率闯两,比如8kHz,會導(dǎo)致粗粒度,AM廣播類型的錄制效果谅将,不過文件會比較醒恰;使用44.1kHz的采樣率(CD質(zhì)量的采樣率)會得到非常高質(zhì)量的你日日饥臂,不過文件就比較大逊躁。對于使用什么采樣率最好沒有一個明確的定義,不過開發(fā)者應(yīng)該盡量使用標(biāo)準(zhǔn)的采樣率隅熙,比如8000稽煤,16000核芽,22050,44100酵熙。最終是我們的耳朵在進(jìn)行判斷轧简。

  • 通道數(shù)

AVNumberOfChannelsKey 指示定義記錄音頻內(nèi)容的通道數(shù),指定默認(rèn)值1意味著使用單聲道錄制绿店,設(shè)置2意味著使用立體聲錄制吉懊。除非使用外部硬件錄制,否則通常選擇單聲道(也就是AVNumberOfChannelsKey=1)假勿。

  • 編碼位元深度

AVEncoderBitDepthHintKey 指示編碼位元深度,從 8 到 32态鳖。

  • 音頻質(zhì)量

AVEncoderAudioQualityKey 指示音頻質(zhì)量转培,可選值有:
AVAudioQualityMin,
AVAudioQualityLow,
AVAudioQualityMedium,
AVAudioQualityHigh,
AVAudioQualityMax。

3.2.1.3 AVAudioRecorder 對象初始化

  • 創(chuàng)建 AVAudioRecorder 需要以下信息:
  1. 用于寫入音頻的本地文件 URL
  2. 用于配置錄音會話鍵值信息的字典
  3. 用于捕捉錯誤的 NSError
  • 初始化時浆竭,需要調(diào)用prepareToRecord 方法執(zhí)行底層 Audio Queue 初始化必要過程浸须,并在指定位置創(chuàng)建文件。
  • 初始化代碼如下:
        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)格式通常是最好的容器格式,因為它和內(nèi)容無關(guān)并且可以保持Core Audio支持的任何音頻格式顺囊。

  • 此外我們需要定義錄音設(shè)置肌索,以便適應(yīng)Apple IMA4作為音頻格式,采樣率44.1kHz,位深度16位特碳,單聲道錄制诚亚。這些設(shè)置考慮了質(zhì)量和文件大小的平衡。

3.2.1.4 錄音文件保存

3.2.2 錄音完整代碼

  • OC 錄音代碼如下:
@interface ViewController ()
@property (nonatomic,strong) AVAudioRecorder *recorder;
@end

@implementation ViewController
 //懶加載
 -(AVAudioRecorder *)recorder{
      if (_recorder == nil) {
          //1.創(chuàng)建沙盒路徑
          NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
          //2.拼接音頻文件
          NSString *filePath = [path stringByAppendingPathComponent:@"123.caf"];
          //3.轉(zhuǎn)換成url  file://
          NSURL *url = [NSURL fileURLWithPath:filePath];
          //4.設(shè)置錄音的參數(shù)
          NSDictionary *settings = @{
                                     /**錄音的質(zhì)量午乓,一般給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);
          //第一個參數(shù)就是你要把錄音保存到哪的url
          //第二個參數(shù)是一些錄音的參數(shù)
          //第三個參數(shù)是錯誤信息
          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
  • Swift版本錄音代碼如下:
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()
    //設(shè)置session類型
    do {
        try session.setCategory(AVAudioSessionCategoryPlayAndRecord)
    } catch let err{
        Dprint("設(shè)置類型失敗:\(err.localizedDescription)")
    }
    //設(shè)置session動作
    do {
        try session.setActive(true)
    } catch let err {
        Dprint("初始化動作失敗:\(err.localizedDescription)")
    }
    //錄音設(shè)置站宗,注意,后面需要轉(zhuǎn)換成NSNumber益愈,如果不轉(zhuǎn)換梢灭,你會發(fā)現(xiàn),無法錄制音頻文件蒸其,我猜測是因為底層還是用OC寫的原因
    let recordSetting: [String: Any] = [AVSampleRateKey: NSNumber(value: 44100.0),//采樣率
        AVFormatIDKey: NSNumber(value: kAudioFormatLinearPCM),//音頻格式
        AVLinearPCMBitDepthKey: NSNumber(value: 16),//采樣位數(shù)
        AVNumberOfChannelsKey: NSNumber(value: 2),//通道數(shù)
        AVEncoderAudioQualityKey: NSNumber(value: AVAudioQuality.min.rawValue)//錄音質(zhì)量
    ];
    //開始錄音
    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)->())?
//結(jié)束錄音
func stopRecord() {
    let session = AVAudioSession.sharedInstance()
    //設(shè)置session類型
    do {
        try session.setCategory(AVAudioSessionCategoryPlayback)
    } catch let err{
        Dprint("設(shè)置類型失敗:\(err.localizedDescription)")
    }
    //設(shè)置session動作
    do {
        try session.setActive(true)
    } catch let err {
        Dprint("初始化動作失敗:\(err.localizedDescription)")
    }
    
    if let recorder = self.recorder {
        if recorder.isRecording {
            Dprint("正在錄音敏释,馬上結(jié)束它,文件保存到了:\(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("正在錄音枣接,馬上結(jié)束它颂暇,文件保存到了:\(mp3file_path)")
            if let block = stopRecordBlock {
                block("/audio.mp3","mp3")
            }
        }else {
            Dprint("沒有錄音,但是依然結(jié)束它")
        }
        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()
    }
}
//進(jìn)度條相關(guān)
func progress()->Double{
    
    return (player?.currentTime)!/(player?.duration)!
}

4. 可視化音頻信號

  • AVAudioRecorder 和 AVAudioPlayer 中最強大和最實用的功能就是對音頻進(jìn)行測量但惶。Audio Metering 可以讓開發(fā)者讀取音頻的平均分貝和峰值分貝塑膠耳鸯,并實用這些數(shù)據(jù)以可視化方式將聲音的大小呈現(xiàn)給最終用戶湿蛔。
  • AVAudioRecorder 和 AVAudioPlayer 都有兩個方法獲取當(dāng)前音頻的平均分貝和峰值分貝數(shù)據(jù),函數(shù)如下:
- (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 */
  • 返回值從 -160dB(靜音) 到 0dB(最大分貝)
  • 獲取值之前要在初始化播放器或記錄器時設(shè)置 meteringEnabled 為 YES才可以支持對音頻測量县爬。
  • 首先需要將 -160 到 0 的分貝值轉(zhuǎn)為 0 到 1 范圍內(nèi)阳啥,代碼如下:
@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

上面代碼創(chuàng)建了一個內(nèi)部數(shù)組,用于保存從計算前的分貝數(shù)到使用一定級別分貝解析之后的轉(zhuǎn)換結(jié)果财喳。這里使用的解析率為-0.2dB.解析等級通過修改MIN_DB和TABLE_SIZE值進(jìn)行調(diào)整察迟。

每個分貝值都通過調(diào)用dbToAmp函數(shù)轉(zhuǎn)換為線性范圍內(nèi)的值,使其處于范圍0(-60dB)到1之間耳高,之后得到一條有這些范圍內(nèi)的值構(gòu)成的平滑曲線扎瓶,開平方計算并保持到內(nèi)部查找表格中。這些值在之后需要時都可以通過調(diào)用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];
}

上面代碼首先調(diào)用錄音器的updateMeters方法概荷。該方法一定要正好在讀取當(dāng)前等級值之前調(diào)用,以保證讀取的級別是最新的碌燕。之后向通道0請求平均值和峰值误证。通道都是0索引的,由于我們使用單聲道錄制修壕,只需要詢問第一個聲道即可愈捅。之后在計量表格中查詢線性聲音強度值并最終創(chuàng)建一個新的THLevelPair實例。

  • 讀取音頻強度值與請求當(dāng)前時間類似慈鸠,當(dāng)需要最新的值時都需要輪詢錄音器蓝谨。我們可以使用NSTimer,但是由于這里會比較頻繁更新用于展示的計量值以保持動畫效果比較平滑林束,所以我們推薦使用CADisplayLink來更新像棘。
  • CADisplayLinkNSTimer類似,不過它可以與顯示刷新率自動同步壶冒。

5. 異常處理

參考書籍:《AV Foundation開發(fā)秘籍》缕题,《音視頻開發(fā)進(jìn)階指南基于Android與iOS平臺的實踐》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胖腾,隨后出現(xiàn)的幾起案子烟零,更是在濱河造成了極大的恐慌,老刑警劉巖咸作,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锨阿,死亡現(xiàn)場離奇詭異,居然都是意外死亡记罚,警方通過查閱死者的電腦和手機(jī)墅诡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桐智,“玉大人末早,你說我怎么就攤上這事烟馅。” “怎么了然磷?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵郑趁,是天一觀的道長。 經(jīng)常有香客問我姿搜,道長寡润,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任舅柜,我火速辦了婚禮梭纹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘致份。我一直安慰自己栗柒,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布知举。 她就那樣靜靜地躺著,像睡著了一般太伊。 火紅的嫁衣襯著肌膚如雪雇锡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天僚焦,我揣著相機(jī)與錄音锰提,去河邊找鬼。 笑死芳悲,一個胖子當(dāng)著我的面吹牛立肘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播名扛,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼谅年,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肮韧?” 一聲冷哼從身側(cè)響起融蹂,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弄企,沒想到半個月后超燃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡拘领,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年意乓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片约素。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡届良,死狀恐怖笆凌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伙窃,我是刑警寧澤菩颖,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站为障,受9級特大地震影響晦闰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳍怨,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一呻右、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鞋喇,春花似錦声滥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至罐韩,卻和暖如春憾赁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背散吵。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工龙考, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人矾睦。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓晦款,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枚冗。 傳聞我的和親對象是個殘疾皇子缓溅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353