大白話(huà)iOS音視頻-01-音頻播放(FFMpeg+AudioUnit)

前言瞎扯

實(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ù)文件如圖

fffmpeg_iOS.png

看看我這個(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ù)

=====>音視頻基礎(chǔ)知識(shí)

1.AudioUnit

1.1大概原理閑扯

啥也不說(shuō). 看看一幅圖.

AudioUnitJG.png

嗯嗯看看圖,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ī).

FFmpeg_AudioUnit.png

如上圖水稻收割機(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ī)出聲.


audioIO.png

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的入口出口等信息, 最后連接.

AudioUnitSet.png

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);    

AVAudioSessionAudio 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];

    1. 設(shè)置I/O的Buffer, Buffer越小則說(shuō)明延遲越低
damijiBuffer.png

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è)圖

AudioUnitJG.png

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),看圖

damijiBufferlei.png

還是打米機(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的入口出口等信息, 最后連接.

    1. 第一步就是拿到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

上面我們的到了AUNodeAUGraph, 現(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 0Element 1, 其中Element 0控制輸出端, Element 1控制輸入端. 同時(shí)每個(gè)Element 又分為Input ScopeOutput Scope. 看圖中APP和Element 1, Element 0的連線, 如果我們只是想播放聲音就將我們的APP與Element 0Input Scope連接起來(lái), 如果我們只是想要通過(guò)麥克風(fēng)錄音我們就將我們的APP與Element 1Output Scope連接起來(lái), 所謂的"連接"代碼里的體現(xiàn)就是設(shè)置兩個(gè)回調(diào)函數(shù)

audioIO.png

本文是干嘛的, 就音頻播放. 所以我們只是想播放聲音就將我們的APP與Element 0Input 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我們拿到的AudioUnit

  • kAudioUnitProperty_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)體AURenderCallbackStructinputProc可以看到函數(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)介紹

QQ20180806-142933@2x.png

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)

ffmpegDecoder01.png

2.3 FFmpeg解碼的流程

ffmpegPlayAudio.png

2.4 API部分說(shuō)明

FFmpeg其他的功能先不說(shuō), 再看看本文的標(biāo)題. 是的我這篇文章是用它來(lái)搞音頻的, 解碼音頻的. 我們這篇文章是播放一個(gè)文件(說(shuō)這句話(huà)是相對(duì)于網(wǎng)絡(luò)流來(lái)說(shuō)),

QQ20180806-142933@2x.png

然后請(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, 第三部用AVFormatContextAVStream,拿到流后第四部用它換解碼器上下文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如圖
ffmpegDu.png

這些視頻呀,音頻呀,字幕呀在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大概講解哈

Demo工程.png

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)在STMediaCacheSTLinkedBlockingQueue

實(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ù)扯下去?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吝羞,一起剝皮案震驚了整個(gè)濱河市兰伤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钧排,老刑警劉巖敦腔,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異恨溜,居然都是意外死亡会烙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)筒捺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人纸厉,你說(shuō)我怎么就攤上這事系吭。” “怎么了颗品?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵肯尺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我躯枢,道長(zhǎng)则吟,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任锄蹂,我火速辦了婚禮氓仲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘得糜。我一直安慰自己敬扛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布朝抖。 她就那樣靜靜地躺著啥箭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪治宣。 梳的紋絲不亂的頭發(fā)上急侥,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天砌滞,我揣著相機(jī)與錄音,去河邊找鬼坏怪。 笑死贝润,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陕悬。 我是一名探鬼主播题暖,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼捉超!你這毒婦竟也來(lái)了胧卤?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拼岳,失蹤者是張志新(化名)和其女友劉穎枝誊,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體惜纸,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叶撒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耐版。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祠够。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖粪牲,靈堂內(nèi)的尸體忽然破棺而出古瓤,到底是詐尸還是另有隱情,我是刑警寧澤腺阳,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布落君,位于F島的核電站,受9級(jí)特大地震影響亭引,放射性物質(zhì)發(fā)生泄漏绎速。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一焙蚓、第九天 我趴在偏房一處隱蔽的房頂上張望纹冤。 院中可真熱鬧,春花似錦购公、人聲如沸赵哲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)枫夺。三九已至,卻和暖如春绘闷,著一層夾襖步出監(jiān)牢的瞬間橡庞,已是汗流浹背较坛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扒最,地道東北人丑勤。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吧趣,于是被迫代替她去往敵國(guó)和親法竞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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