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

前言瞎扯

實(shí)際關(guān)于利用FFmpeg+AudioUnit,相關(guān)文章是有的,但是還是有所不足, 較多是只言片語有的沒有Demo,所以我還是要寫這么一篇, 我這篇的特點(diǎn)是, 閑扯中讓各位(讓我自己~)從最基本的概念->能搞出東西.

Demo地址當(dāng)然直接下載下來是不能跑的你要安裝我的===>編譯iOS能用的FFmpeg靜態(tài)庫這篇文章里說的把編譯好的FFmpeg拖到我的工程了,然后Build Setting —-> 搜索Header Search Paths添加$(PROJECT_DIR)/AudioUnitPlayerDemo/ffmpeg/include

基礎(chǔ)知識(shí)不太熟的同學(xué)看看我的這篇文章
=====>音視頻基礎(chǔ)知識(shí), 只是為看懂本文的話, 看音頻部分就好啦.

看我這篇文章你能干嘛?

你可以完成一個(gè)音頻播放Demo. 用AudioUnit播放一個(gè)mp3, aac, 這樣的文件, 或者視頻文件的音頻也就是說只播放MP4文件聲音. 播放一幀一幀的音頻數(shù)據(jù)(實(shí)際上是音頻裸數(shù)據(jù)PCM, 而PCM是沒有的概念的.PCM說的采樣..). 播放本地文件呢,是為后面播放網(wǎng)絡(luò)過來的數(shù)據(jù)打個(gè)基礎(chǔ), 因?yàn)?code>解碼,解封裝, AudioUnit 相關(guān)API等相關(guān)知識(shí)是直播也好播本地文件也好是相同的代碼, 多的只是處理網(wǎng)絡(luò)流部分的邏輯.

大概怎么做?

FFmpeg解碼mp3, aac, MP4, 這類的封裝格式拿到裸數(shù)據(jù)(pcm), 然后AudioUnit

材料

FFmpeg + AudioUnit + 音視頻文件

FFmpeg是編譯iOS能用的靜態(tài)庫文件如圖

fffmpeg_iOS.png

看看我這個(gè)文章,如果你本地沒有編譯好的.
===>編譯iOS能用的FFmpeg靜態(tài)庫

往下就是具體邏輯講解了, 默認(rèn)你懂了關(guān)于音頻的基礎(chǔ)知識(shí)和已經(jīng)編譯好iOS能用的靜態(tài)庫了哈, 那啥要不再看看
===>編譯iOS能用的FFmpeg靜態(tài)庫

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

1.AudioUnit

1.1大概原理閑扯

啥也不說. 看看一幅圖.

AudioUnitJG.png

嗯嗯看看圖,AudioUnit在下去就是硬件了.用它處理音視頻數(shù)據(jù)確實(shí)略微"復(fù)雜"."復(fù)雜"的話功能就會(huì)有點(diǎn)騷.

AudioUnit 就一個(gè)小孩, 需要一直喂東西.我要做的就是不斷喂他東西.....或者說AudioUnit就是一臺(tái)機(jī)器,它生產(chǎn)的產(chǎn)品是聲音, 我們要做的就是不斷的給他填原料, 本篇文章就當(dāng)他是打米機(jī)好了, FFmpeg就是水稻收割機(jī).

FFmpeg_AudioUnit.png

如上圖水稻收割機(jī)(FFmpeg)從田里(音視頻文件)收獲稻谷(PCM),然后進(jìn)過我們調(diào)度給打米機(jī)(AudioUnit),然后生產(chǎn)大米(聲音)..

打米機(jī)如圖右邊那個(gè)漏斗是填稻谷的, 然后下面中間的出口產(chǎn)生大米,右邊產(chǎn)生米糠(稻谷的殼). 當(dāng)我們買來零件組裝好一臺(tái)打米機(jī)插上電就可以讓它運(yùn)行起來你要是填稻米它就生產(chǎn)大米,你沒稻米填給它就在那白跑著浪費(fèi)電,打米機(jī)它不管稻米哪來的它只要人給它填稻米,是不是水稻收割機(jī)從田里采集的還是農(nóng)民通過人工采集的它不管, 它只是說給我稻米給我電我給你大米. 然后AudioUnit 這家伙跟它一個(gè)意思.

如圖,AudioUnit跟打米機(jī)一樣也是一個(gè)漏斗填音頻(aac,pcm)數(shù)據(jù)給他,然后它讓揚(yáng)聲器或者耳機(jī)出聲.


audioIO.png

1.2 相關(guān)API混臉熟

好啦廢話說了那么多了,基本上知道AudioUnit是一個(gè)什么樣尿性的家伙了.下面說具體的類、結(jié)構(gòu)體、函數(shù)、方法什么的了.

原料有:AVAudioSession, AudioComponentDescription, AUNode, AUGraph, AudioStreamBasicDescription, AURenderCallbackStruct 差不多這些結(jié)構(gòu)體類啥的(并不是~),

函數(shù)方法~(先寫兩個(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);

開始有點(diǎn)代碼了哈, 上面提到的類結(jié)構(gòu)體方法函數(shù)先混個(gè)臉熟吧, 花30秒過一遍....

1.3操作過程和具體API講解

AudioUnit的使用一句話講解是這樣的: 首先使用AVAudioSession會(huì)話用來管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription)確定AudioUnit的類型(AudioUnit能做很多事情的,不同的類型干不同的事,我們這里是找能播放音頻的那個(gè)),然后通過 AUNode, AUGraph拿到我們的AudioUnit, 然后設(shè)置AudioUnit的入口出口等信息, 最后連接.

AudioUnitSet.png

1.3.1 AVAudioSession

在iOS的音視頻開發(fā)中, 使用具體API之前都會(huì)先創(chuàng)建一個(gè)會(huì)話, 這里也不例外.這是必須的第一步, 你在使用AudioUnit之前必須先創(chuàng)建會(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è)是類,一個(gè)是一個(gè)函數(shù),使用起來還是很不同的, 我們這里用前者. 我們將用一個(gè)包裝類來使用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越小則說明延遲越低
damijiBuffer.png

AudioUnit的buffer就好像打米機(jī)的稻谷漏斗. 如圖打米機(jī)自帶的漏斗填滿稻谷可能需要1分鐘打完, 所以我們需要快1分鐘后就要再往里面填稻谷, 如果我們換成左邊那個(gè)更小的漏斗(buffer)可能40秒就打完了, 換個(gè)大的就時(shí)間長點(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, 具體看采樣率.
采樣率哪來?FFmpeg讀音視頻文件得到.FFmpeg給的.

具體體現(xiàn)函數(shù)(看里面的注釋~)


/**

這就是我們給AudioUnit喂食的函數(shù), 也就是AudioUnit的漏斗,你上buffer設(shè)置的越小呢AudioUnit調(diào)用這個(gè)函數(shù)的頻率就越高, 然后每次問你要的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寫,網(wǎng)差也會(huì)超過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è)沒啥好說的直接上代碼)


[audioSession setPreferredSampleRate:sampleRate error:nil];


  • 5.激活A(yù)VAudioSession

[audioSession setActive:YES error:nil];

到這里哈和AudioUnit API還沒半毛錢關(guān)系的哈, 但是再看一下這個(gè)圖

AudioUnitJG.png

AudioUnit下面就是驅(qū)動(dòng)和硬件了意思是它是跟硬件和驅(qū)動(dòng)直接大交道的, 所以使用AudioUnit之前必須要?jiǎng)?chuàng)建一個(gè)會(huì)話管理獲取硬件相關(guān)信息.

雖然沒有用到AudioUnit, 但是卻對(duì)其有很大的影響,代碼不多就⑤步~

1.3.2 創(chuàng)建AudioUnit

實(shí)際上AudioUnit是一個(gè)大類名稱,看圖

damijiBufferlei.png

還是打米機(jī)哈,不好意思哈我真的覺得這家伙和打米機(jī)好像([捂臉] 哈哈哈哈~), 如圖打米機(jī)有很多種型號(hào),有的打米機(jī)不只有"打米"的功能還有將小麥加工成面粉呢(并沒有真的見過那種機(jī)器~瞎扯的).

AudioUnit也一樣,它分為五大類,每個(gè)大類下面又有具體子類.它不只是播放聲音這么簡單(就好像打米機(jī)并不只是簡單的將稻谷去殼一樣, 有的大米比較白是打米機(jī)給他拋光了~AudioUnit有做錄音播放的, 有做混音的等等...)但他們統(tǒng)一叫AudioUnit, 我們這篇文章用到的是I/O Units這個(gè)大類下的RemoteIO和Format Converter Units大類下AUConverter


I/O Units這個(gè)大類類型是`kAudioUnitType_Output`
 RemoteIO: 子類類型是`kAudioUnitSubType_RemoteIO`


Format Converter Units這個(gè)大類類型是`kAudioUnitType_FormatConverter`
 AUConverter: 子類類型是`kAudioUnitSubType_AUConverter`

I/O嘛就是播放和錄音嘛,我們只用它的播放功能.還記得上面[audioSession setCategory:AVAudioSessionCategoryPlayback];這個(gè)沒,如果你還要錄音就得改一下

再看一下1.3開頭說的這句話

AudioUnit的使用一句話講解是這樣的: 首先使用AVAudioSession會(huì)話用來管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription)確定AudioUnit的類型,然后通過 AUNode, AUGraph拿到我們的AudioUnit, 然后設(shè)置AudioUnit的入口出口等信息, 最后連接.

    1. 第一步就是拿到AUGraph,AUNode

首先要說的是我們是通過AUGraph,AUNode去換AudioUnit, AUNode我們可以理解為他是AudioUnit的包裝類.

我們上面說了, AudioUnit是分很多種的, 我們要用到的是I/O 和 Format Converter Units, 后者是做格式轉(zhuǎn)換的, 因?yàn)槲覀冇肍Fmpeg解碼出來的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í)候需要指定 類型(Type), 子類型(subtype), 以及廠商(Manufacture). 這里體現(xiàn)在AudioComponentDescription設(shè)置上.

類型(Type)就是大類了,上面簡單介紹過的東西
子類型(subtype)就是該大類型下面的子類型
廠商(Manufacture)一般情況比較固定, 直接寫成kAudioUnitManufacturer_Apple

  • 2.獲取我們要的AudioUnit

上面我們的到了AUNodeAUGraph, 現(xiàn)在我們可以通過他們召喚出真正的AudioUnit了, 操作順序是先打開 AUGraph, 然后再召喚,順序不能變.


AudioUnit convertUnit;
OSStatus status = noErr;
status = NewAUGraph(&_auGraph);

status = AUGraphAddNode(_auGraph,
                            &ioDescription,
                            &_convertUnit);

// 打開AUGraph, 其實(shí)打開AUGraph的過程也是間接實(shí)例化AUGraph中所有的AUNode.
//注意, 必須在獲取AudioUnit之前打開整個(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,還有些零件沒裝好.就好像打米機(jī)的漏斗和出口都沒裝.

1.3.3 設(shè)置AudioUnit

再看一下1.3開頭說的這句話, 之所以重復(fù)就是要知道我們離目的地有多遠(yuǎn),當(dāng)前在哪

AudioUnit的使用一句話講解是這樣的: 首先使用AVAudioSession會(huì)話用來管理獲取硬件信息, 然后利用一個(gè)描述結(jié)構(gòu)體(AudioComponentDescription)確定AudioUnit的類型,然后通過 AUNode, AUGraph拿到我們的AudioUnit, 然后設(shè)置AudioUnit的入口出口等信息, 最后連接.

再來看看看下面那個(gè)圖
沒錯(cuò)這就是我們使用的I/O Unit原理圖, 我們用的是I/O Unit大類下的RemoteIO. I就是輸入端,O是輸出端. 輸入端一般是麥克風(fēng)或者網(wǎng)絡(luò)流, 輸出端是揚(yáng)聲器或者耳機(jī). 就好像打米機(jī)的漏斗或者大米出口, 到目前為止漏斗出口兩個(gè)組件還沒有裝上的,我們得把他倆裝上.

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

audioIO.png

本文是干嘛的, 就音頻播放. 所以我們只是想播放聲音就將我們的APP與Element 0Input Scope連接起來, 連接之前我們要告訴等會(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ù)..通過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)做音視頻開發(fā): 不論音頻還是視頻的API都會(huì)接觸到很多StreamBasic Description. 該Description就是用來描述音視頻具體格式的. 下面是上述代碼的分析

  • bytesPersample采樣深度(采樣精度, 量化格式), 三個(gè)都是一個(gè)意思哈.

  • mFormatID 參數(shù)可用來指定音頻的編碼格式. 此處指定音頻的編碼格式為PCM格式.什么樣的音頻數(shù)據(jù), 這里我設(shè)置裸數(shù)據(jù)PCM

  • mSampleRate 采樣率

  • mChannelsPerFrame每一幀里面有多少聲道, 實(shí)際上就是問聲道數(shù).

  • mFormatFlags 是用來描述聲音表示格式的參數(shù), 代碼中的參數(shù)kLinearPCMFormatFlagIsSignedInteger指定每個(gè)Sample的表示格式是SInt16格式, ..
  • mFramesPerPacket 這個(gè)說的是每一幀里面有多少個(gè)包. PCM數(shù)據(jù)是沒有壓縮過的裸數(shù)據(jù), 所以是一幀一個(gè)包, 壓縮編碼后的數(shù)據(jù)例如AAC, 一幀數(shù)據(jù)對(duì)應(yīng)1024個(gè)包. 所以這里我們寫1
    以后我們?nèi)绻菇oAudioUnit的不是裸數(shù)據(jù)PCM的話,如果是AAC就寫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ù)的, 就拿雙聲道來說, 兩個(gè)聲道就是兩路兩個(gè), 我們可以將兩路數(shù)據(jù)放到一個(gè)數(shù)組里給AudioUnit(這就是交叉), 我們也可以分兩個(gè)數(shù)組給AudioUnit, 到底怎么給了實(shí)際是看mFormatFlags, kLinearPCMFormatFlagIsSignedInteger這樣不只是說PCM數(shù)據(jù)是用SInt16表示還有交叉的PCM的意思. 那誰有是非交叉了? 這里先不說...那具體是影響到哪里了,答:是影響到AudioUnit問我們要數(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ù)用多少位來表示, 前面已經(jīng)提到過每個(gè)采樣使用SInt16來表示, 所以這里是使用8乘以每個(gè)采樣的字節(jié)數(shù)來賦值

*** 描述結(jié)構(gòu)體弄完了下一步我們就來設(shè)置Element 0的Input Scope***


status = AudioUnitSetProperty(
_convertUnit, 
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0, 
&_clientFormat16int,
sizeof(_clientFormat16int));

  • _convertUnit我們拿到的AudioUnit

  • kAudioUnitProperty_StreamFormat 說的是本次調(diào)用AudioUnitSetProperty函數(shù)時(shí)做連接, 然后告訴AudioUnit連接的數(shù)據(jù)流.AudioUnitSetProperty函數(shù)可以做很多事情的具體什么事情就看第二參數(shù)的值是什么了

  • kAudioUnitScope_Input就是上面說的Input Scope

  • 0就是Element 0

  • _clientFormat16int就是描述了

前面說了,我們需要兩個(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ù)不是亂寫的, 我們點(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跑起來


    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)原理就說完了.

實(shí)際上還有AudioUnit的分類沒有說可以看我這篇文章AudioUnit的分類

2.FFmpeg操作

首先默認(rèn)你們已經(jīng)按照我的這篇文章===>編譯iOS能用的FFmpeg靜態(tài)庫做好了靜態(tài)庫,工程相關(guān)配置也是按照文章做好了的哈..

然后再回顧一下=====>音視頻基礎(chǔ)知識(shí), 如下圖封裝格式===>編碼數(shù)據(jù)===>原始數(shù)據(jù), 我們用FFmpeg做解碼也都是按照這個(gè)順序使用它的相關(guān)數(shù)據(jù)結(jié)構(gòu)和相關(guān)函數(shù)來的. 下面1-3小節(jié)是相關(guān)介紹

QQ20180806-142933@2x.png

2.1 FFmpeg數(shù)據(jù)結(jié)構(gòu)簡介

  • 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部分說明

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

QQ20180806-142933@2x.png

然后請(qǐng)?jiān)倏匆槐檫@個(gè)圖, AudioUnit要的是音頻采樣數(shù)據(jù)PCM, 我們現(xiàn)在有的是什么? 是一個(gè)mp4文件或者一個(gè)mp3文件, 是文件! FFmpeg我們用它干嘛? 我們用它扣出PCM數(shù)據(jù),然后喂給AudioUnit. 說到頭就是解碼. 解碼就是用的解碼流程里的avcodec_decode_audio4

扣PCM喂給AudioUnit, 到底怎么扣?
還是前面那個(gè)套路哈, 一句話簡單說就是: 不管用FFmpeg解碼音頻也會(huì)視頻也好,第一步都是先注冊(cè), 第二步就是去拿封裝格式上下文AVFormatContext, 第三部用AVFormatContextAVStream,拿到流后第四部用它換解碼器上下文AVCodecContext, 然后第五步我們就要用解碼器上下文去讀取編碼數(shù)據(jù)AVPacket, 最后第七步我們解碼編碼數(shù)據(jù)通過avcodec_decode_audio4函數(shù)換取PCM裸數(shù)據(jù)AVFrame

  • AVFormatContext封裝格式講解

關(guān)于封裝格式的話, 先看代碼吧



_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)上下來部星爺賭圣, 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)里面就是我們說的AVStream, 看見上圖中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);

我們通過上面的代碼拿到了序號(hào), 我們就可以通過序號(hào)去拿音頻流數(shù)據(jù)了, 這里是拿音頻序號(hào)因?yàn)楸疚氖茄芯恳纛l播放的,所以Demo里也只會(huì)出現(xiàn)如上拿音頻的API視頻呀字幕呀本文不會(huì)介紹. 下面是通過序號(hào)拿音頻流


AVStream *audioStream = _avFormatContext->streams[_stream_index];


  • AVCodecContext解碼器上下文

我們要拿流數(shù)據(jù)AVStream是用來換取解碼器和解碼器上下文的.為什么有了解碼器還要什么解碼器上下文?因?yàn)槲覀兒竺娼獯a用到的函數(shù)avcodec_decode_audio4要傳的是上下文,第二解碼器上下文里面包含了解碼器


    // 獲得音頻流的解碼器上下文
    _avCodecContext = audioStream->codec;
    // 根據(jù)解碼器上下文找到解碼器
    AVCodec *avCodec = avcodec_find_decoder(_avCodecContext->codec_id);
    
    // 打開解碼器
    result = avcodec_open2(_avCodecContext, avCodec, NULL);

3. 工程Demo大概講解哈

Demo工程.png

AudioUnit主要邏輯在ST_AudioOutput里面, AVAudioSession使用ST_AudioSession這個(gè)封裝類

FFmpeg使用在STFFmpegLocalAudioDecoder

然后用到了生產(chǎn)模式消費(fèi)模式搞了一個(gè)線程不間斷的生產(chǎn)數(shù)據(jù),然后放到隊(duì)列中,系統(tǒng)快消耗完了就去補(bǔ)貨,具體體現(xiàn)在STMediaCacheSTLinkedBlockingQueue

實(shí)際上重要的先看懂上面的流程圖比較總要, FFmpeg API的使用實(shí)際上套路都差不多, 注冊(cè)找上下文找流找解碼器解碼....我個(gè)人覺得FFmpeg按文理來說我覺得它屬于文科.....那有人問了"我最開始應(yīng)該怎么學(xué)?"我的覺得哈買本我不是跟誰誰打廣告哈, 書是比較系統(tǒng)性的網(wǎng)上的多的是之言片語少了從頭到尾,我這篇也是.第二是FFmpeg源碼里的examples, 就好像ffmpeg-3.4.2源碼里examples的位置是/ffmpeg-3.4.2/doc/examples, 想學(xué)哪個(gè)學(xué)哪個(gè)差不多的功能都有了, 然后就是網(wǎng)上的各種博客了.

我這篇文章是看了<<FFmpeg從入門到精通>>和<<音視頻開發(fā)進(jìn)階指南>>還有雷霄驊博士博客,當(dāng)然也閱讀了些博客, 實(shí)際上我這篇文章的Demo也是改寫了<<音視頻開發(fā)進(jìn)階指南>>書里的例子, 因?yàn)橹皇荄emo嘛多少還有些問題, 希望能幫助你吧. 如果覺得還行記得給我點(diǎn)個(gè)贊,表揚(yáng)我一下,啊哈哈哈哈哈~然后我將大白話iOS音視頻繼續(xù)扯下去?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容