前言瞎扯
實(shí)際關(guān)于利用FFmpeg
+AudioUnit
,相關(guān)文章是有的,但是還是有所不足, 較多是只言片語(yǔ)有的沒(méi)有Demo,所以我還是要寫(xiě)這么一篇, 我這篇的特點(diǎn)是, 閑扯中讓各位(讓我自己~)從最基本的概念
->能搞出東西
.
Demo地址當(dāng)然直接下載下來(lái)是不能跑的你要安裝我的===>編譯iOS能用的FFmpeg靜態(tài)庫(kù)這篇文章里說(shuō)的把編譯好的FFmpeg拖到我的工程了,然后Build Setting
—-> 搜索Header Search Paths
添加$(PROJECT_DIR)/AudioUnitPlayerDemo/ffmpeg/include
基礎(chǔ)知識(shí)不太熟的同學(xué)看看我的這篇文章
=====>音視頻基礎(chǔ)知識(shí), 只是為看懂本文的話(huà), 看音頻部分就好啦.
看我這篇文章你能干嘛?
你可以完成一個(gè)音頻播放Demo. 用AudioUnit播放一個(gè)mp3
, aac
, 這樣的文件, 或者視頻文件的音頻也就是說(shuō)只播放MP4文件聲音. 播放一幀一幀的音頻數(shù)據(jù)(實(shí)際上是音頻裸數(shù)據(jù)PCM, 而PCM是沒(méi)有幀
的概念的.PCM說(shuō)的采樣..). 播放本地文件呢,是為后面播放網(wǎng)絡(luò)過(guò)來(lái)的數(shù)據(jù)打個(gè)基礎(chǔ), 因?yàn)?code>解碼,解封裝
, AudioUnit 相關(guān)API
等相關(guān)知識(shí)是直播也好播本地文件也好是相同的代碼, 多的只是處理網(wǎng)絡(luò)流部分的邏輯.
大概怎么做?
FFmpeg解碼mp3
, aac
, MP4
, 這類(lèi)的封裝格式
拿到裸數(shù)據(jù)(pcm)
, 然后喂
給AudioUnit
材料
FFmpeg
+ AudioUnit
+ 音視頻文件
FFmpeg
是編譯iOS能用的靜態(tài)庫(kù)文件如圖
看看我這個(gè)文章,如果你本地沒(méi)有編譯好的.
===>編譯iOS能用的FFmpeg靜態(tài)庫(kù)
往下就是具體邏輯講解了, 默認(rèn)你懂了關(guān)于音頻的基礎(chǔ)知識(shí)和已經(jīng)編譯好iOS能用的靜態(tài)庫(kù)了哈, 那啥要不再看看
===>編譯iOS能用的FFmpeg靜態(tài)庫(kù)
1.AudioUnit
1.1大概原理閑扯
啥也不說(shuō). 看看一幅圖.
嗯嗯看看圖,AudioUnit在下去就是硬件了.用它處理音視頻數(shù)據(jù)確實(shí)略微"復(fù)雜"."復(fù)雜"的話(huà)功能就會(huì)有點(diǎn)騷.
AudioUnit 就一個(gè)小孩, 需要一直喂東西.我要做的就是不斷喂他東西.
....或者說(shuō)AudioUnit就是一臺(tái)機(jī)器,它生產(chǎn)的產(chǎn)品是聲音, 我們要做的就是不斷的給他填原料
, 本篇文章就當(dāng)他是打米機(jī)
好了, FFmpeg
就是水稻收割機(jī).
如上圖水稻收割機(jī)
(FFmpeg
)從田里
(音視頻文件
)收獲稻谷
(PCM
),然后進(jìn)過(guò)我們調(diào)度給打米機(jī)
(AudioUnit
),然后生產(chǎn)大米
(聲音
)..
打米機(jī)
如圖右邊那個(gè)漏斗是填稻谷的, 然后下面中間的出口產(chǎn)生大米,右邊產(chǎn)生米糠(稻谷的殼). 當(dāng)我們買(mǎi)來(lái)零件組裝好一臺(tái)打米機(jī)插上電就可以讓它運(yùn)行起來(lái)你要是填稻米它就生產(chǎn)大米,你沒(méi)稻米填給它就在那白跑著浪費(fèi)電,打米機(jī)
它不管稻米
哪來(lái)的它只要人給它填稻米,是不是水稻收割機(jī)
從田里采集的還是農(nóng)民通過(guò)人工采集的它不管, 它只是說(shuō)給我稻米給我電我給你大米
. 然后AudioUnit
這家伙跟它一個(gè)意思.
如圖,AudioUnit跟打米機(jī)一樣也是一個(gè)漏斗填音頻(aac,pcm)數(shù)據(jù)給他,然后它讓揚(yáng)聲器或者耳機(jī)出聲.
1.2 相關(guān)API混臉熟
好啦廢話(huà)說(shuō)了那么多了,基本上知道AudioUnit是一個(gè)什么樣尿性的家伙了.下面說(shuō)具體的類(lèi)、結(jié)構(gòu)體、函數(shù)它改、方法什么的了.
原料有:AVAudioSession
, AudioComponentDescription
, AUNode
, AUGraph
, AudioStreamBasicDescription
, AURenderCallbackStruct
差不多這些結(jié)構(gòu)體類(lèi)啥的(并不是~),
函數(shù)方法~(先寫(xiě)兩個(gè)):
AUGraphNodeInfo( AUGraph inGraph,
AUNode inNode,
AudioComponentDescription * __nullable outDescription,
AudioUnit __nullable * __nullable outAudioUnit) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
AudioUnitSetProperty( AudioUnit inUnit,
AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement,
const void * __nullable inData,
UInt32 inDataSize)
__OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);
開(kāi)始有點(diǎn)代碼了哈, 上面提到的類(lèi)結(jié)構(gòu)體方法函數(shù)先混個(gè)臉熟吧, 花30秒過(guò)一遍....
1.3操作過(guò)程和具體API講解
AudioUnit的使用一句話(huà)講解是這樣的: 首先使用AVAudioSession
會(huì)話(huà)用來(lái)管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription
)確定AudioUnit
的類(lèi)型(AudioUnit能做很多事情的,不同的類(lèi)型干不同的事,我們這里是找能播放音頻的那個(gè)),然后通過(guò) AUNode
, AUGraph
拿到我們的AudioUnit
, 然后設(shè)置AudioUnit
的入口出口等信息, 最后連接.
1.3.1 AVAudioSession
在iOS的音視頻開(kāi)發(fā)中, 使用具體API之前都會(huì)先創(chuàng)建一個(gè)會(huì)話(huà), 這里也不例外.這是必須的第一步, 你在使用AudioUnit之前必須先創(chuàng)建會(huì)話(huà)并設(shè)置相關(guān)參數(shù).
AVAudioSession 用于管理與獲取iOS設(shè)備音頻的硬件信息, 并且是以單例的形式存在.iOS7以前是使用Audio Session
兩個(gè)實(shí)際上是干一件事.就是管理與獲取iOS設(shè)備音頻的硬件信息
, 你的聲音是揚(yáng)聲器播勒還是耳機(jī)了,是藍(lán)牙耳機(jī)了還是插線耳機(jī)了這些信息都由他管, 舉個(gè)例子:你用揚(yáng)聲器播的好好的然后你插耳機(jī)了這時(shí)要他做一定邏輯處理.
AVAudioSession
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
Audio Session
AudioSessionInitialize(
NULL,// Run loop (NULL = main run loop)
kCFRunLoopDefaultMode, // Run loop mode
(void(*)(void*,UInt32))XXXXXX, // Interruption callback
NULL);
AVAudioSession
和 Audio Session
一個(gè)是類(lèi),一個(gè)是一個(gè)函數(shù),使用起來(lái)還是很不同的, 我們這里用前者. 我們將用一個(gè)包裝類(lèi)來(lái)使用AVAudioSession, 下面是具體介紹
- 1.獲取AVAudioSession實(shí)例
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
- 2.設(shè)置硬件能力
我們要做什么? 看我的標(biāo)題,我們只要播放聲音,我們想要iPhone手機(jī)播放聲音.然后我們?cè)O(shè)置AVAudioSessionCategoryPlayback
, 如果我們要手機(jī)采集又播放就是AVAudioSessionCategoryPlayAndRecord
[audioSession setCategory:AVAudioSessionCategoryPlayback];
- 設(shè)置I/O的Buffer,
Buffer
越小則說(shuō)明延遲越低
- 設(shè)置I/O的Buffer,
AudioUnit
的buffer就好像打米機(jī)的稻谷漏斗. 如圖打米機(jī)自帶的漏斗填滿(mǎn)稻谷可能需要1分鐘打完, 所以我們需要快1分鐘后就要再往里面填稻谷, 如果我們換成左邊那個(gè)更小的漏斗(buffer
)可能40秒就打完了, 換個(gè)大的就時(shí)間長(zhǎng)點(diǎn). 小的漏斗呢就需要人不斷的加稻谷, 大的就不需要那么頻繁.
[audioSession setPreferredIOBufferDuration:bufferDuration error:nil];
PCM數(shù)據(jù)是1024個(gè)采樣一個(gè)包, 所以一般就用1024采樣點(diǎn)的時(shí)間, 所以這里的值最大是1024/sampleRate(采樣率), 只能比這個(gè)小, 越小的buffer, 延遲就越低, 一般設(shè)置成1024/sampleRate(采樣率)
就行了.
如果采樣率是44100, 就是1024/44100=0.023, 具體看采樣率.
采樣率哪來(lái)?FFmpeg讀音視頻文件得到.FFmpeg給的.
具體體現(xiàn)函數(shù)(看里面的注釋~)
/**
這就是我們給AudioUnit喂食的函數(shù), 也就是AudioUnit的漏斗,你上buffer設(shè)置的越小呢AudioUnit調(diào)用這個(gè)函數(shù)的頻率就越高, 然后每次問(wèn)你要的inNumberFrames個(gè)數(shù)就越少
, 多少的基礎(chǔ)標(biāo)準(zhǔn)就是"1024/sampleRate"的值,實(shí)際上最大可以是"1024/sampleRate*1.4",
最小嘛就是"1.0/sampleRate"就是1buffer大小, 知道就行,然后設(shè)置成"1024/sampleRate"就行了, 這都是毫秒級(jí)別的了,各種直播協(xié)議延遲能到1秒就燒香拜佛了.(就算直接TCP協(xié)議用socket寫(xiě),網(wǎng)差也會(huì)超過(guò)3秒4秒啥的,閑扯的~)
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &STInputRenderCallback;
*/
typedef OSStatus
(*AURenderCallback)( void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData);
- 4.設(shè)置采樣率(這個(gè)沒(méi)啥好說(shuō)的直接上代碼)
[audioSession setPreferredSampleRate:sampleRate error:nil];
- 5.激活A(yù)VAudioSession
[audioSession setActive:YES error:nil];
到這里哈和AudioUnit
API還沒(méi)半毛錢(qián)關(guān)系的哈, 但是再看一下這個(gè)圖
AudioUnit
下面就是驅(qū)動(dòng)和硬件了意思是它是跟硬件和驅(qū)動(dòng)直接大交道的, 所以使用AudioUnit之前必須要?jiǎng)?chuàng)建一個(gè)會(huì)話(huà)管理獲取硬件相關(guān)信息.
雖然沒(méi)有用到AudioUnit, 但是卻對(duì)其有很大的影響,代碼不多就⑤步~
1.3.2 創(chuàng)建AudioUnit
實(shí)際上AudioUnit
是一個(gè)大類(lèi)名稱(chēng),看圖
還是打米機(jī)哈,不好意思哈我真的覺(jué)得這家伙和打米機(jī)好像([捂臉] 哈哈哈哈~), 如圖打米機(jī)有很多種型號(hào),有的打米機(jī)不只有"打米"的功能還有將小麥加工成面粉呢(并沒(méi)有真的見(jiàn)過(guò)那種機(jī)器~瞎扯的).
AudioUnit
也一樣,它分為五大類(lèi),每個(gè)大類(lèi)下面又有具體子類(lèi).它不只是播放聲音這么簡(jiǎn)單(就好像打米機(jī)并不只是簡(jiǎn)單的將稻谷去殼一樣, 有的大米比較白是打米機(jī)給他拋光了~AudioUnit有做錄音播放的, 有做混音的等等...)但他們統(tǒng)一叫AudioUnit
, 我們這篇文章用到的是I/O Units
這個(gè)大類(lèi)下的RemoteIO和Format Converter Units
大類(lèi)下AUConverter
I/O Units這個(gè)大類(lèi)類(lèi)型是`kAudioUnitType_Output`
RemoteIO: 子類(lèi)類(lèi)型是`kAudioUnitSubType_RemoteIO`
Format Converter Units這個(gè)大類(lèi)類(lèi)型是`kAudioUnitType_FormatConverter`
AUConverter: 子類(lèi)類(lèi)型是`kAudioUnitSubType_AUConverter`
I/O嘛就是播放和錄音嘛,我們只用它的播放功能.還記得上面[audioSession setCategory:AVAudioSessionCategoryPlayback];
這個(gè)沒(méi),如果你還要錄音就得改一下
再看一下1.3開(kāi)頭說(shuō)的這句話(huà)
AudioUnit的使用一句話(huà)講解是這樣的: 首先使用
AVAudioSession
會(huì)話(huà)用來(lái)管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription
)確定AudioUnit
的類(lèi)型,然后通過(guò)AUNode
,AUGraph
拿到我們的AudioUnit
, 然后設(shè)置AudioUnit
的入口出口等信息, 最后連接.
- 第一步就是拿到
AUGraph
,AUNode
- 第一步就是拿到
首先要說(shuō)的是我們是通過(guò)AUGraph
,AUNode
去換AudioUnit, AUNode
我們可以理解為他是AudioUnit的包裝類(lèi).
我們上面說(shuō)了, AudioUnit是分很多種的, 我們要用到的是I/O 和 Format Converter Units, 后者是做格式轉(zhuǎn)換的, 因?yàn)槲覀冇肍Fmpeg解碼出來(lái)的PCM是SInt16表示的, AudioUnit要的Float32,所以要格式轉(zhuǎn)換一下.所以要用到Format Converter Units
入下面代碼我們得到兩個(gè)AUNode
, 也就是兩個(gè)AudioUnit
SStatus status = noErr;
status = NewAUGraph(&_auGraph);
AudioComponentDescription ioDescription;
bzero(&ioDescription, sizeof(ioDescription));
ioDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioDescription.componentType = kAudioUnitType_Output;
ioDescription.componentSubType = kAudioUnitSubType_RemoteIO;
status = AUGraphAddNode(_auGraph,
&ioDescription,
&_ioNNode);
CheckStatus(status, @"AUGraphAddNode create error", YES);
AudioComponentDescription converDescription;
bzero(&converDescription, sizeof(converDescription));
converDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
converDescription.componentType = kAudioUnitType_FormatConverter;
converDescription.componentSubType = kAudioUnitSubType_AUConverter;
status = AUGraphAddNode(_auGraph,
&converDescription,
&_convertNote);
CheckStatus(status, @"AUGraphAddNode _convertNote create error", YES);
構(gòu)建AudioUnit的時(shí)候需要指定 類(lèi)型(Type), 子類(lèi)型(subtype), 以及廠商(Manufacture). 這里體現(xiàn)在AudioComponentDescription
設(shè)置上.
類(lèi)型(Type)就是大類(lèi)了,上面簡(jiǎn)單介紹過(guò)的東西
子類(lèi)型(subtype)就是該大類(lèi)型下面的子類(lèi)型
廠商(Manufacture)一般情況比較固定, 直接寫(xiě)成kAudioUnitManufacturer_Apple
- 2.獲取我們要的AudioUnit
上面我們的到了AUNode
和AUGraph
, 現(xiàn)在我們可以通過(guò)他們召喚出真正的AudioUnit
了, 操作順序是先打開(kāi) AUGraph
, 然后再召喚,順序不能變.
AudioUnit convertUnit;
OSStatus status = noErr;
status = NewAUGraph(&_auGraph);
status = AUGraphAddNode(_auGraph,
&ioDescription,
&_convertUnit);
// 打開(kāi)AUGraph, 其實(shí)打開(kāi)AUGraph的過(guò)程也是間接實(shí)例化AUGraph中所有的AUNode.
//注意, 必須在獲取AudioUnit之前打開(kāi)整個(gè)AUGraph, 否則我們將不能從對(duì)應(yīng)的AUNode中獲取正確的AudioUnit
status = AUGraphOpen(_auGraph);
status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_convertUnit);
status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_ioUnit);
至此我們拿到AudioUnit
.
實(shí)際上是一個(gè)不完整的AudioUnit,還有些零件沒(méi)裝好.就好像打米機(jī)的漏斗和出口都沒(méi)裝.
1.3.3 設(shè)置AudioUnit
再看一下1.3開(kāi)頭說(shuō)的這句話(huà), 之所以重復(fù)就是要知道我們離目的地有多遠(yuǎn),當(dāng)前在哪
AudioUnit的使用一句話(huà)講解是這樣的: 首先使用
AVAudioSession
會(huì)話(huà)用來(lái)管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription
)確定AudioUnit
的類(lèi)型,然后通過(guò)AUNode
,AUGraph
拿到我們的AudioUnit
, 然后設(shè)置AudioUnit
的入口出口等信息, 最后連接.
再來(lái)看看看下面那個(gè)圖
沒(méi)錯(cuò)這就是我們使用的I/O Unit
原理圖, 我們用的是I/O Unit
大類(lèi)下的RemoteIO
. I就是輸入端,O是輸出端. 輸入端一般是麥克風(fēng)或者網(wǎng)絡(luò)流, 輸出端是揚(yáng)聲器或者耳機(jī). 就好像打米機(jī)的漏斗或者大米出口, 到目前為止漏斗
和出口
兩個(gè)組件還沒(méi)有裝上的,我們得把他倆裝上.
如圖RemoteIO Unit
分為Element 0
和Element 1
, 其中Element 0
控制輸出端, Element 1
控制輸入端. 同時(shí)每個(gè)Element 又分為Input Scope
和Output Scope
. 看圖中APP和Element 1, Element 0
的連線, 如果我們只是想播放聲音就將我們的APP與Element 0
的Input Scope
連接起來(lái), 如果我們只是想要通過(guò)麥克風(fēng)錄音我們就將我們的APP與Element 1
的Output Scope
連接起來(lái), 所謂的"連接"代碼里的體現(xiàn)就是設(shè)置兩個(gè)回調(diào)函數(shù)
本文是干嘛的, 就音頻播放. 所以我們只是想播放聲音就將我們的APP與
Element 0的
Input Scope連接起來(lái)
, 連接之前我們要告訴等會(huì)傳輸給他的音頻數(shù)據(jù)的參數(shù)(告訴他是什么樣的音頻)
有關(guān)AudioUnit的設(shè)置都是使用AudioUnitSetProperty
函數(shù)
extern OSStatus
AudioUnitSetProperty( AudioUnit inUnit,
AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement,
const void * __nullable inData,
UInt32 inDataSize)
__OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0);
做連接之前我們得先告訴AudioUnit我們給它的音頻的相關(guān)參數(shù).采樣率是多少,聲道多少,是什么音頻數(shù)據(jù)等等參數(shù)..通過(guò)AudioStreamBasicDescription
結(jié)構(gòu)體設(shè)置:
AudioStreamBasicDescription _clientFormat16int;
UInt32 bytesPersample = sizeof(SInt16);
bzero(&_clientFormat16int, sizeof(_clientFormat16int));
_clientFormat16int.mFormatID = kAudioFormatLinearPCM;
_clientFormat16int.mSampleRate = _sampleRate;
_clientFormat16int.mChannelsPerFrame = _channels;
_clientFormat16int.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
_clientFormat16int.mFramesPerPacket = 1;
_clientFormat16int.mBytesPerPacket = bytesPersample * _channels;
_clientFormat16int.mBytesPerFrame = bytesPersample * _channels;
_clientFormat16int.mBitsPerChannel = 8 * bytesPersample;
上面這段代碼展示了如何填充AudioStreamBasicDescription
結(jié)構(gòu)體, 其實(shí)在iOS平臺(tái)做音視頻開(kāi)發(fā): 不論音頻還是視頻的API都會(huì)接觸到很多StreamBasic Description. 該Description就是用來(lái)描述音視頻具體格式的.
下面是上述代碼的分析
bytesPersample
是采樣深度(采樣精度, 量化格式)
, 三個(gè)都是一個(gè)意思哈.mFormatID
參數(shù)可用來(lái)指定音頻的編碼格式. 此處指定音頻的編碼格式為PCM格式.什么樣的音頻數(shù)據(jù), 這里我設(shè)置裸數(shù)據(jù)PCM
mSampleRate
采樣率mChannelsPerFrame
每一幀里面有多少聲道, 實(shí)際上就是問(wèn)聲道數(shù).
-
mFormatFlags
是用來(lái)描述聲音表示格式的參數(shù), 代碼中的參數(shù)kLinearPCMFormatFlagIsSignedInteger
指定每個(gè)Sample的表示格式是SInt16格式, ..
-
mFramesPerPacket
這個(gè)說(shuō)的是每一幀里面有多少個(gè)包. PCM數(shù)據(jù)是沒(méi)有壓縮過(guò)的裸數(shù)據(jù), 所以是一幀一個(gè)包, 壓縮編碼后的數(shù)據(jù)例如AAC, 一幀數(shù)據(jù)對(duì)應(yīng)1024個(gè)包. 所以這里我們寫(xiě)1
以后我們?nèi)绻菇oAudioUnit的不是裸數(shù)據(jù)PCM的話(huà),如果是AAC就寫(xiě)1024
AudioStreamBasicDescription audio_desc = { 0 };
audio_desc.mFormatID = kAudioFormatMPEG4AAC;
audio_desc.mFormatFlags = kMPEG4Object_AAC_LC;
audio_desc.mFramesPerPacket = 1024;
mBytesPerPacket
每一個(gè)包里面有多少個(gè)字節(jié), 這里就涉及到你是怎樣填數(shù)據(jù)的, 就拿雙聲道來(lái)說(shuō), 兩個(gè)聲道就是兩路兩個(gè), 我們可以將兩路數(shù)據(jù)放到一個(gè)數(shù)組里給AudioUnit(這就是交叉), 我們也可以分兩個(gè)數(shù)組給AudioUnit, 到底怎么給了實(shí)際是看mFormatFlags
,kLinearPCMFormatFlagIsSignedInteger
這樣不只是說(shuō)PCM數(shù)據(jù)是用SInt16表示還有交叉的PCM的意思. 那誰(shuí)有是非交叉了? 這里先不說(shuō)...那具體是影響到哪里了,答:是影響到AudioUnit問(wèn)我們要數(shù)據(jù)的那個(gè)回調(diào)函數(shù).的AudioBufferList * __nullable ioData) (你可以理解這家伙就是打米機(jī)的填稻谷那個(gè)漏斗)
, 實(shí)際上我們?yōu)榱朔奖銛?shù)據(jù)填入, 不管是播放聲音也錄音也會(huì), 都是用的交叉(因?yàn)榉奖?...) 所以就是bytesPersample * _channels;
mBytesPerFrame
每一幀有多少個(gè)字節(jié), 因?yàn)檫@里是一幀一包, 所以就也是bytesPersample * _channels;
- mBitsPerChannel 表示的是
一個(gè)聲道的
音頻數(shù)據(jù)用多少位來(lái)表示, 前面已經(jīng)提到過(guò)每個(gè)采樣使用SInt16來(lái)表示, 所以這里是使用8乘以每個(gè)采樣的字節(jié)數(shù)
來(lái)賦值
*** 描述結(jié)構(gòu)體弄完了下一步我們就來(lái)設(shè)置Element 0的Input Scope***
status = AudioUnitSetProperty(
_convertUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&_clientFormat16int,
sizeof(_clientFormat16int));
_convertUnit
我們拿到的AudioUnitkAudioUnitProperty_StreamFormat
說(shuō)的是本次調(diào)用AudioUnitSetProperty
函數(shù)時(shí)做連接, 然后告訴AudioUnit連接的數(shù)據(jù)流.AudioUnitSetProperty
函數(shù)可以做很多事情的具體什么事情就看第二參數(shù)的值是什么了kAudioUnitScope_Input
就是上面說(shuō)的Input Scope
0
就是Element 0
了_clientFormat16int
就是描述了
前面說(shuō)了,我們需要兩個(gè)AudioUnit
一個(gè)"I/O"的一個(gè)"convert"的, 并且也已經(jīng)拿到了, 也設(shè)置好"convert", 下面就可以做連接.
OSStatus status = noErr;
status = AUGraphConnectNodeInput(_auGraph, _convertNote, 0, _ioNNode, 0);
CheckStatus(status, @"Could not connect I/O node input to mixer node input", YES);
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &STInputRenderCallback;
callbackStruct.inputProcRefCon = (__bridge void *)self;
status = AudioUnitSetProperty(_convertUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
CheckStatus(status, @"Could not set render callback on mixer input scope, element 1", YES);
"I/O"才有輸入功能, 但是數(shù)據(jù)需要轉(zhuǎn)換所以先連接 _convertNote和_ioNNode. AUGraphConnectNodeInput(_auGraph, _convertNote, 0, _ioNNode, 0);
然后就是最重要的一步回調(diào)函數(shù)的設(shè)置.,前面一系列操作相當(dāng)于是制作打米機(jī)的漏斗,現(xiàn)在我們就要將漏斗裝上.&STInputRenderCallback;
這個(gè)回調(diào)函數(shù)就是真正的AudioUnit的"漏斗", AudioUnit會(huì)按照我們?cè)O(shè)置的時(shí)間不斷的調(diào)用此回調(diào)函數(shù)向我們索要音頻數(shù)據(jù), 函數(shù)如下
static OSStatus STInputRenderCallback(void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData)
{
NSLog(@"====> inBusNumber:%u inNumberFrames:%u", (unsigned int)inBusNumber, inNumberFrames);
ST_AudioOutput *audioOutput = (__bridge id)inRefCon;
return [audioOutput renderData:ioData
atTimeStamp:inTimeStamp
forElement:inBusNumber
numberFrames:inNumberFrames
flags:ioActionFlags];
}
這個(gè)函數(shù)不是亂寫(xiě)的, 我們點(diǎn)擊結(jié)構(gòu)體AURenderCallbackStruct
的inputProc
可以看到函數(shù)原型如下. 我們要做的是實(shí)現(xiàn)該函數(shù), 函數(shù)名由我們自己定義.我講函數(shù)名定義為STInputRenderCallback
你們也可以隨意定義改函數(shù)名, 函數(shù)體是固定的.
typedef OSStatus
(*AURenderCallback)( void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData);
連接完成后進(jìn)行最后一步的操作, 啟動(dòng)
,讓AudioUnit跑起來(lái)
CAShow(_auGraph);
status = AUGraphInitialize(_auGraph);
CheckStatus(status, @"Could not initialize AUGraph", YES);
執(zhí)行完上面一步后AudioUnit就會(huì)不斷的調(diào)用回調(diào)函數(shù), 我們要做的就是不斷的給它音頻數(shù)據(jù)
至此有關(guān)AudioUnit操作相關(guān)原理就說(shuō)完了.
實(shí)際上還有AudioUnit的分類(lèi)沒(méi)有說(shuō)可以看我這篇文章AudioUnit的分類(lèi)
2.FFmpeg操作
首先默認(rèn)你們已經(jīng)按照我的這篇文章===>編譯iOS能用的FFmpeg靜態(tài)庫(kù)做好了靜態(tài)庫(kù),工程相關(guān)配置也是按照文章做好了的哈..
然后再回顧一下=====>音視頻基礎(chǔ)知識(shí), 如下圖封裝格式
===>編碼數(shù)據(jù)
===>原始數(shù)據(jù)
, 我們用FFmpeg做解碼也都是按照這個(gè)順序使用它的相關(guān)數(shù)據(jù)結(jié)構(gòu)和相關(guān)函數(shù)來(lái)的. 下面1-3小節(jié)是相關(guān)介紹
2.1 FFmpeg數(shù)據(jù)結(jié)構(gòu)簡(jiǎn)介
- AVFormatContext
封裝格式上下文結(jié)構(gòu)體,也是統(tǒng)領(lǐng)全局的結(jié)構(gòu)體癞谒,保存了視頻文件封裝 格式相關(guān)信息。
- AVInputFormat
每種封裝格式(例如FLV, MKV, MP4, AVI)對(duì)應(yīng)一個(gè)該結(jié)構(gòu)體夯尽。
AVStream
視頻文件中每個(gè)視頻(音頻)流對(duì)應(yīng)一個(gè)該結(jié)構(gòu)體袱箱。AVCodecContext
編碼器上下文結(jié)構(gòu)體,保存了視頻(音頻)編解碼相關(guān)信息式曲。AVCodec
每種視頻(音頻)編解碼器(例如H.264解碼器)對(duì)應(yīng)一個(gè)該結(jié)構(gòu)體妨托。AVPacket
存儲(chǔ)一幀壓縮編碼數(shù)據(jù)。AVFrame
存儲(chǔ)一幀解碼后像素(采樣)數(shù)據(jù)
2.2 FFmpeg解碼的數(shù)據(jù)結(jié)構(gòu)
2.3 FFmpeg解碼的流程
2.4 API部分說(shuō)明
FFmpeg其他的功能先不說(shuō), 再看看本文的標(biāo)題. 是的我這篇文章是用它來(lái)搞音頻的, 解碼音頻的. 我們這篇文章是播放一個(gè)文件(說(shuō)這句話(huà)是相對(duì)于網(wǎng)絡(luò)流來(lái)說(shuō)),
然后請(qǐng)?jiān)倏匆槐檫@個(gè)圖, AudioUnit
要的是音頻采樣數(shù)據(jù)PCM
, 我們現(xiàn)在有的是什么? 是一個(gè)mp4文件或者一個(gè)mp3文件, 是文件! FFmpeg
我們用它干嘛? 我們用它扣出PCM數(shù)據(jù),然后喂給AudioUnit
. 說(shuō)到頭就是解碼. 解碼就是用的解碼流程里的avcodec_decode_audio4
扣PCM喂給AudioUnit
, 到底怎么扣?
還是前面那個(gè)套路哈, 一句話(huà)簡(jiǎn)單說(shuō)就是: 不管用FFmpeg
解碼音頻也會(huì)視頻也好,第一步都是先注冊(cè), 第二步就是去拿封裝格式上下文AVFormatContext
, 第三部用AVFormatContext
換AVStream
,拿到流后第四部用它換解碼器上下文AVCodecContext
, 然后第五步我們就要用解碼器上下文去讀取編碼數(shù)據(jù)AVPacket
, 最后第七步我們解碼編碼數(shù)據(jù)通過(guò)avcodec_decode_audio4
函數(shù)換取PCM裸數(shù)據(jù)AVFrame
- AVFormatContext封裝格式講解
關(guān)于封裝格式的話(huà), 先看代碼吧
_avFormatContext = avformat_alloc_context();
int result = avformat_open_input(&_avFormatContext,
[audioFileStr UTF8String],
NULL,
NULL);
int result = avformat_find_stream_info(_avFormatContext, NULL);
其實(shí)這塊都不用怎么解釋我們相信大家都能看懂
- AVStream音頻流講解
先看上面的封裝格式
那個(gè)圖.封裝格式
由音頻編碼數(shù)據(jù)和視頻編碼數(shù)據(jù)組成(有的還有字幕數(shù)據(jù)), 我從網(wǎng)上下來(lái)部星爺
的賭圣
, mkv格式的電影, 然后使用ffmpeg命令ffprobe -show_format /Users/codew/Desktop/賭圣.mkv
看看封裝格式的組成. 它由7部分組成, 視頻編碼數(shù)據(jù)一個(gè)Video: h264 (High)
, 有兩個(gè)音頻編碼數(shù)據(jù)都是Audio: aac (HE-AACv2)
, 然后四個(gè)字幕數(shù)據(jù)Subtitle: subrip
如圖
這些視頻呀,音頻呀,字幕呀在FFmpeg數(shù)據(jù)結(jié)構(gòu)里面就是我們說(shuō)的AVStream
, 看見(jiàn)上圖中Stream #0:0(chi)
, Stream #0:1(chi)
等等了嗎? 這些流是有序號(hào)的. 我們要用這個(gè)些流我們得找到流對(duì)應(yīng)的序號(hào)就像下面這樣
_stream_index = av_find_best_stream(_avFormatContext,
AVMEDIA_TYPE_AUDIO,
-1,
-1,
NULL,
0);
我們通過(guò)上面的代碼拿到了序號(hào), 我們就可以通過(guò)序號(hào)去拿音頻流數(shù)據(jù)了, 這里是拿音頻序號(hào)因?yàn)楸疚氖茄芯恳纛l播放的,所以Demo里也只會(huì)出現(xiàn)如上拿音頻的API視頻呀字幕呀本文不會(huì)介紹. 下面是通過(guò)序號(hào)拿音頻流
AVStream *audioStream = _avFormatContext->streams[_stream_index];
- AVCodecContext解碼器上下文
我們要拿流數(shù)據(jù)AVStream
是用來(lái)?yè)Q取解碼器和解碼器上下文的.為什么有了解碼器還要什么解碼器上下文?因?yàn)槲覀兒竺娼獯a用到的函數(shù)avcodec_decode_audio4
要傳的是上下文,第二解碼器上下文里面包含了解碼器
// 獲得音頻流的解碼器上下文
_avCodecContext = audioStream->codec;
// 根據(jù)解碼器上下文找到解碼器
AVCodec *avCodec = avcodec_find_decoder(_avCodecContext->codec_id);
// 打開(kāi)解碼器
result = avcodec_open2(_avCodecContext, avCodec, NULL);
3. 工程Demo大概講解哈
AudioUnit主要邏輯在ST_AudioOutput
里面, AVAudioSession使用ST_AudioSession
這個(gè)封裝類(lèi)
FFmpeg使用在STFFmpegLocalAudioDecoder
然后用到了生產(chǎn)模式消費(fèi)模式搞了一個(gè)線程不間斷的生產(chǎn)數(shù)據(jù),然后放到隊(duì)列中,系統(tǒng)快消耗完了就去補(bǔ)貨,具體體現(xiàn)在STMediaCache
和STLinkedBlockingQueue
后
實(shí)際上重要的先看懂上面的流程圖比較總要, FFmpeg API的使用實(shí)際上套路都差不多, 注冊(cè)找上下文找流找解碼器解碼....我個(gè)人覺(jué)得FFmpeg按文理來(lái)說(shuō)我覺(jué)得它屬于文科.....那有人問(wèn)了"我最開(kāi)始應(yīng)該怎么學(xué)?"我的覺(jué)得哈買(mǎi)本我不是跟誰(shuí)誰(shuí)打廣告哈, 書(shū)是比較系統(tǒng)性的網(wǎng)上的多的是之言片語(yǔ)少了從頭到尾,我這篇也是.第二是FFmpeg源碼里的examples
, 就好像ffmpeg-3.4.2源碼里examples
的位置是/ffmpeg-3.4.2/doc/examples
, 想學(xué)哪個(gè)學(xué)哪個(gè)差不多的功能都有了, 然后就是網(wǎng)上的各種博客了.
我這篇文章是看了<<FFmpeg從入門(mén)到精通>>和<<音視頻開(kāi)發(fā)進(jìn)階指南>>還有雷霄驊博士博客,當(dāng)然也閱讀了些博客, 實(shí)際上我這篇文章的Demo也是改寫(xiě)了<<音視頻開(kāi)發(fā)進(jìn)階指南>>書(shū)里的例子, 因?yàn)橹皇荄emo嘛多少還有些問(wèn)題, 希望能幫助你吧. 如果覺(jué)得還行記得給我點(diǎn)個(gè)贊,表?yè)P(yáng)我一下,啊哈哈哈哈哈~然后我將大白話(huà)iOS音視頻繼續(xù)扯下去?