大白话iOS音视频-01-音频播放(FFMpeg+AudioUnit)

前言瞎扯

实际关于利用FFmpeg+AudioUnit,相关文章是有的,但是还是有所不足, 较多是只言片语有的没有Demo,所以我还是要写这么一篇, 我这篇的特点是, 闲扯中让各位(让我自己~)从最基本的概念->能搞出东西.

Demo地址当然直接下载下来是不能跑的你要安装我的===>编译iOS能用的FFmpeg静态库这篇文章里说的把编译好的FFmpeg拖到我的工程了,然后Build Setting —-> 搜索Header Search Paths添加$(PROJECT_DIR)/AudioUnitPlayerDemo/ffmpeg/include

基础知识不太熟的同学看看我的这篇文章
=====>音视频基础知识, 只是为看懂本文的话, 看音频部分就好啦.

看我这篇文章你能干嘛?

你可以完成一个音频播放Demo. 用AudioUnit播放一个mp3, aac, 这样的文件, 或者视频文件的音频也就是说只播放MP4文件声音. 播放一帧一帧的音频数据(实际上是音频裸数据PCM, 而PCM是没有的概念的.PCM说的采样..). 播放本地文件呢,是为后面播放网络过来的数据打个基础, 因为解码,解封装, AudioUnit 相关API等相关知识是直播也好播本地文件也好是相同的代码, 多的只是处理网络流部分的逻辑.

大概怎么做?

FFmpeg解码mp3, aac, MP4, 这类的封装格式拿到裸数据(pcm), 然后AudioUnit

材料

FFmpeg + AudioUnit + 音视频文件

FFmpeg是编译iOS能用的静态库文件如图

fffmpeg_iOS.png

看看我这个文章,如果你本地没有编译好的.
===>编译iOS能用的FFmpeg静态库

往下就是具体逻辑讲解了, 默认你懂了关于音频的基础知识和已经编译好iOS能用的静态库了哈, 那啥要不再看看
===>编译iOS能用的FFmpeg静态库

=====>音视频基础知识

1.AudioUnit

1.1大概原理闲扯

啥也不说. 看看一幅图.

AudioUnitJG.png

嗯嗯看看图,AudioUnit在下去就是硬件了.用它处理音视频数据确实略微"复杂"."复杂"的话功能就会有点骚.

AudioUnit 就一个小孩, 需要一直喂东西.我要做的就是不断喂他东西.....或者说AudioUnit就是一台机器,它生产的产品是声音, 我们要做的就是不断的给他填原料, 本篇文章就当他是打米机好了, FFmpeg就是水稻收割机.

FFmpeg_AudioUnit.png

如上图水稻收割机(FFmpeg)从田里(音视频文件)收获稻谷(PCM),然后进过我们调度给打米机(AudioUnit),然后生产大米(声音)..

打米机如图右边那个漏斗是填稻谷的, 然后下面中间的出口产生大米,右边产生米糠(稻谷的壳). 当我们买来零件组装好一台打米机插上电就可以让它运行起来你要是填稻米它就生产大米,你没稻米填给它就在那白跑着浪费电,打米机它不管稻米哪来的它只要人给它填稻米,是不是水稻收割机从田里采集的还是农民通过人工采集的它不管, 它只是说给我稻米给我电我给你大米. 然后AudioUnit 这家伙跟它一个意思.

如图,AudioUnit跟打米机一样也是一个漏斗填音频(aac,pcm)数据给他,然后它让扬声器或者耳机出声.


audioIO.png

1.2 相关API混脸熟

好啦废话说了那么多了,基本上知道AudioUnit是一个什么样尿性的家伙了.下面说具体的类、结构体、函数、方法什么的了.

原料有:AVAudioSession, AudioComponentDescription, AUNode, AUGraph, AudioStreamBasicDescription, AURenderCallbackStruct 差不多这些结构体类啥的(并不是~),

函数方法~(先写两个):

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

开始有点代码了哈, 上面提到的类结构体方法函数先混个脸熟吧, 花30秒过一遍....

1.3操作过程和具体API讲解

AudioUnit的使用一句话讲解是这样的: 首先使用AVAudioSession会话用来管理获取硬件信息, 然后利用一个描述结构体(AudioComponentDescription)确定AudioUnit的类型(AudioUnit能做很多事情的,不同的类型干不同的事,我们这里是找能播放音频的那个),然后通过 AUNode, AUGraph拿到我们的AudioUnit, 然后设置AudioUnit的入口出口等信息, 最后连接.

AudioUnitSet.png

1.3.1 AVAudioSession

在iOS的音视频开发中, 使用具体API之前都会先创建一个会话, 这里也不例外.这是必须的第一步, 你在使用AudioUnit之前必须先创建会话并设置相关参数.

AVAudioSession 用于管理与获取iOS设备音频的硬件信息, 并且是以单例的形式存在.iOS7以前是使用Audio Session两个实际上是干一件事.就是管理与获取iOS设备音频的硬件信息, 你的声音是扬声器播勒还是耳机了,是蓝牙耳机了还是插线耳机了这些信息都由他管, 举个例子:你用扬声器播的好好的然后你插耳机了这时要他做一定逻辑处理.

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一个是类,一个是一个函数,使用起来还是很不同的, 我们这里用前者. 我们将用一个包装类来使用AVAudioSession, 下面是具体介绍

  • 1.获取AVAudioSession实例

AVAudioSession * audioSession = [AVAudioSession  sharedInstance];

  • 2.设置硬件能力

我们要做什么? 看我的标题,我们只要播放声音,我们想要iPhone手机播放声音.然后我们设置AVAudioSessionCategoryPlayback, 如果我们要手机采集又播放就是AVAudioSessionCategoryPlayAndRecord


[audioSession setCategory:AVAudioSessionCategoryPlayback];

    1. 设置I/O的Buffer, Buffer越小则说明延迟越低
damijiBuffer.png

AudioUnit的buffer就好像打米机的稻谷漏斗. 如图打米机自带的漏斗填满稻谷可能需要1分钟打完, 所以我们需要快1分钟后就要再往里面填稻谷, 如果我们换成左边那个更小的漏斗(buffer)可能40秒就打完了, 换个大的就时间长点. 小的漏斗呢就需要人不断的加稻谷, 大的就不需要那么频繁.


 [audioSession setPreferredIOBufferDuration:bufferDuration error:nil];


PCM数据是1024个采样一个包, 所以一般就用1024采样点的时间, 所以这里的值最大是1024/sampleRate(采样率), 只能比这个小, 越小的buffer, 延迟就越低, 一般设置成1024/sampleRate(采样率)就行了.
如果采样率是44100, 就是1024/44100=0.023, 具体看采样率.
采样率哪来?FFmpeg读音视频文件得到.FFmpeg给的.

具体体现函数(看里面的注释~)


/**

这就是我们给AudioUnit喂食的函数, 也就是AudioUnit的漏斗,你上buffer设置的越小呢AudioUnit调用这个函数的频率就越高, 然后每次问你要的inNumberFrames个数就越少
, 多少的基础标准就是"1024/sampleRate"的值,实际上最大可以是"1024/sampleRate*1.4", 
最小嘛就是"1.0/sampleRate"就是1buffer大小, 知道就行,然后设置成"1024/sampleRate"就行了, 这都是毫秒级别的了,各种直播协议延迟能到1秒就烧香拜佛了.(就算直接TCP协议用socket写,网差也会超过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.设置采样率(这个没啥好说的直接上代码)


[audioSession setPreferredSampleRate:sampleRate error:nil];


  • 5.激活AVAudioSession

[audioSession setActive:YES error:nil];

到这里哈和AudioUnit API还没半毛钱关系的哈, 但是再看一下这个图

AudioUnitJG.png

AudioUnit下面就是驱动和硬件了意思是它是跟硬件和驱动直接大交道的, 所以使用AudioUnit之前必须要创建一个会话管理获取硬件相关信息.

虽然没有用到AudioUnit, 但是却对其有很大的影响,代码不多就⑤步~

1.3.2 创建AudioUnit

实际上AudioUnit是一个大类名称,看图

damijiBufferlei.png

还是打米机哈,不好意思哈我真的觉得这家伙和打米机好像([捂脸] 哈哈哈哈~), 如图打米机有很多种型号,有的打米机不只有"打米"的功能还有将小麦加工成面粉呢(并没有真的见过那种机器~瞎扯的).

AudioUnit也一样,它分为五大类,每个大类下面又有具体子类.它不只是播放声音这么简单(就好像打米机并不只是简单的将稻谷去壳一样, 有的大米比较白是打米机给他抛光了~AudioUnit有做录音播放的, 有做混音的等等...)但他们统一叫AudioUnit, 我们这篇文章用到的是I/O Units这个大类下的RemoteIO和Format Converter Units大类下AUConverter


I/O Units这个大类类型是`kAudioUnitType_Output`
 RemoteIO: 子类类型是`kAudioUnitSubType_RemoteIO`


Format Converter Units这个大类类型是`kAudioUnitType_FormatConverter`
 AUConverter: 子类类型是`kAudioUnitSubType_AUConverter`

I/O嘛就是播放和录音嘛,我们只用它的播放功能.还记得上面[audioSession setCategory:AVAudioSessionCategoryPlayback];这个没,如果你还要录音就得改一下

再看一下1.3开头说的这句话

AudioUnit的使用一句话讲解是这样的: 首先使用AVAudioSession会话用来管理获取硬件信息, 然后利用一个描述结构体(AudioComponentDescription)确定AudioUnit的类型,然后通过 AUNode, AUGraph拿到我们的AudioUnit, 然后设置AudioUnit的入口出口等信息, 最后连接.

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

首先要说的是我们是通过AUGraph,AUNode去换AudioUnit, AUNode我们可以理解为他是AudioUnit的包装类.

我们上面说了, AudioUnit是分很多种的, 我们要用到的是I/O 和 Format Converter Units, 后者是做格式转换的, 因为我们用FFmpeg解码出来的PCM是SInt16表示的, AudioUnit要的Float32,所以要格式转换一下.所以要用到Format Converter Units

入下面代码我们得到两个AUNode, 也就是两个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);

构建AudioUnit的时候需要指定 类型(Type), 子类型(subtype), 以及厂商(Manufacture). 这里体现在AudioComponentDescription设置上.

类型(Type)就是大类了,上面简单介绍过的东西
子类型(subtype)就是该大类型下面的子类型
厂商(Manufacture)一般情况比较固定, 直接写成kAudioUnitManufacturer_Apple

  • 2.获取我们要的AudioUnit

上面我们的到了AUNodeAUGraph, 现在我们可以通过他们召唤出真正的AudioUnit了, 操作顺序是先打开 AUGraph, 然后再召唤,顺序不能变.


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

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

// 打开AUGraph, 其实打开AUGraph的过程也是间接实例化AUGraph中所有的AUNode.
//注意, 必须在获取AudioUnit之前打开整个AUGraph, 否则我们将不能从对应的AUNode中获取正确的AudioUnit

status = AUGraphOpen(_auGraph);

status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_convertUnit);

status = AUGraphNodeInfo(_auGraph, _ioNNode, NULL, &_ioUnit);


至此我们拿到AudioUnit.

实际上是一个不完整的AudioUnit,还有些零件没装好.就好像打米机的漏斗和出口都没装.

1.3.3 设置AudioUnit

再看一下1.3开头说的这句话, 之所以重复就是要知道我们离目的地有多远,当前在哪

AudioUnit的使用一句话讲解是这样的: 首先使用AVAudioSession会话用来管理获取硬件信息, 然后利用一个描述结构体(AudioComponentDescription)确定AudioUnit的类型,然后通过 AUNode, AUGraph拿到我们的AudioUnit, 然后设置AudioUnit的入口出口等信息, 最后连接.

再来看看看下面那个图
没错这就是我们使用的I/O Unit原理图, 我们用的是I/O Unit大类下的RemoteIO. I就是输入端,O是输出端. 输入端一般是麦克风或者网络流, 输出端是扬声器或者耳机. 就好像打米机的漏斗或者大米出口, 到目前为止漏斗出口两个组件还没有装上的,我们得把他俩装上.

如图RemoteIO Unit分为Element 0Element 1, 其中Element 0控制输出端, Element 1控制输入端. 同时每个Element 又分为Input ScopeOutput Scope. 看图中APP和Element 1, Element 0的连线, 如果我们只是想播放声音就将我们的APP与Element 0Input Scope连接起来, 如果我们只是想要通过麦克风录音我们就将我们的APP与Element 1Output Scope连接起来, 所谓的"连接"代码里的体现就是设置两个回调函数

audioIO.png

本文是干嘛的, 就音频播放. 所以我们只是想播放声音就将我们的APP与Element 0Input Scope连接起来, 连接之前我们要告诉等会传输给他的音频数据的参数(告诉他是什么样的音频)

有关AudioUnit的设置都是使用AudioUnitSetProperty函数


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我们给它的音频的相关参数.采样率是多少,声道多少,是什么音频数据等等参数..通过AudioStreamBasicDescription结构体设置:



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结构体, 其实在iOS平台做音视频开发: 不论音频还是视频的API都会接触到很多StreamBasic Description. 该Description就是用来描述音视频具体格式的. 下面是上述代码的分析

  • bytesPersample采样深度(采样精度, 量化格式), 三个都是一个意思哈.

  • mFormatID 参数可用来指定音频的编码格式. 此处指定音频的编码格式为PCM格式.什么样的音频数据, 这里我设置裸数据PCM

  • mSampleRate 采样率

  • mChannelsPerFrame每一帧里面有多少声道, 实际上就是问声道数.

  • mFormatFlags 是用来描述声音表示格式的参数, 代码中的参数kLinearPCMFormatFlagIsSignedInteger指定每个Sample的表示格式是SInt16格式, ..
  • mFramesPerPacket 这个说的是每一帧里面有多少个包. PCM数据是没有压缩过的裸数据, 所以是一帧一个包, 压缩编码后的数据例如AAC, 一帧数据对应1024个包. 所以这里我们写1
    以后我们如果喂给AudioUnit的不是裸数据PCM的话,如果是AAC就写1024

AudioStreamBasicDescription audio_desc = { 0 };
audio_desc.mFormatID           = kAudioFormatMPEG4AAC;
audio_desc.mFormatFlags        = kMPEG4Object_AAC_LC; 
audio_desc.mFramesPerPacket    = 1024;

  • mBytesPerPacket每一个包里面有多少个字节, 这里就涉及到你是怎样填数据的, 就拿双声道来说, 两个声道就是两路两个, 我们可以将两路数据放到一个数组里给AudioUnit(这就是交叉), 我们也可以分两个数组给AudioUnit, 到底怎么给了实际是看mFormatFlags, kLinearPCMFormatFlagIsSignedInteger这样不只是说PCM数据是用SInt16表示还有交叉的PCM的意思. 那谁有是非交叉了? 这里先不说...那具体是影响到哪里了,答:是影响到AudioUnit问我们要数据的那个回调函数.的AudioBufferList * __nullable ioData) (你可以理解这家伙就是打米机的填稻谷那个漏斗), 实际上我们为了方便数据填入, 不管是播放声音也录音也会, 都是用的交叉(因为方便....) 所以就是bytesPersample * _channels;

  • mBytesPerFrame每一帧有多少个字节, 因为这里是一帧一包, 所以就也是bytesPersample * _channels;

  • mBitsPerChannel 表示的是一个声道的音频数据用多少位来表示, 前面已经提到过每个采样使用SInt16来表示, 所以这里是使用8乘以每个采样的字节数来赋值

*** 描述结构体弄完了下一步我们就来设置Element 0的Input Scope***


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

  • _convertUnit我们拿到的AudioUnit

  • kAudioUnitProperty_StreamFormat 说的是本次调用AudioUnitSetProperty函数时做连接, 然后告诉AudioUnit连接的数据流.AudioUnitSetProperty函数可以做很多事情的具体什么事情就看第二参数的值是什么了

  • kAudioUnitScope_Input就是上面说的Input Scope

  • 0就是Element 0

  • _clientFormat16int就是描述了

前面说了,我们需要两个AudioUnit一个"I/O"的一个"convert"的, 并且也已经拿到了, 也设置好"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"才有输入功能, 但是数据需要转换所以先连接 _convertNote和_ioNNode. AUGraphConnectNodeInput(_auGraph, _convertNote, 0, _ioNNode, 0);

然后就是最重要的一步回调函数的设置.,前面一系列操作相当于是制作打米机的漏斗,现在我们就要将漏斗装上.&STInputRenderCallback; 这个回调函数就是真正的AudioUnit的"漏斗", AudioUnit会按照我们设置的时间不断的调用此回调函数向我们索要音频数据, 函数如下


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

这个函数不是乱写的, 我们点击结构体AURenderCallbackStructinputProc可以看到函数原型如下. 我们要做的是实现该函数, 函数名由我们自己定义.我讲函数名定义为STInputRenderCallback你们也可以随意定义改函数名, 函数体是固定的.


typedef OSStatus
(*AURenderCallback)(    void *                          inRefCon,
                        AudioUnitRenderActionFlags *    ioActionFlags,
                        const AudioTimeStamp *          inTimeStamp,
                        UInt32                          inBusNumber,
                        UInt32                          inNumberFrames,
                        AudioBufferList * __nullable    ioData);


连接完成后进行最后一步的操作, 启动,让AudioUnit跑起来


    CAShow(_auGraph);
    status = AUGraphInitialize(_auGraph);
    CheckStatus(status, @"Could not initialize AUGraph", YES);

执行完上面一步后AudioUnit就会不断的调用回调函数, 我们要做的就是不断的给它音频数据

至此有关AudioUnit操作相关原理就说完了.

实际上还有AudioUnit的分类没有说可以看我这篇文章AudioUnit的分类

2.FFmpeg操作

首先默认你们已经按照我的这篇文章===>编译iOS能用的FFmpeg静态库做好了静态库,工程相关配置也是按照文章做好了的哈..

然后再回顾一下=====>音视频基础知识, 如下图封装格式===>编码数据===>原始数据, 我们用FFmpeg做解码也都是按照这个顺序使用它的相关数据结构和相关函数来的. 下面1-3小节是相关介绍

QQ20180806-142933@2x.png

2.1 FFmpeg数据结构简介

  • AVFormatContext

封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装 格式相关信息。

  • AVInputFormat

每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。

  • AVStream
    视频文件中每个视频(音频)流对应一个该结构体。

  • AVCodecContext
    编码器上下文结构体,保存了视频(音频)编解码相关信息。

  • AVCodec
    每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。

  • AVPacket
    存储一帧压缩编码数据。

  • AVFrame
    存储一帧解码后像素(采样)数据

2.2 FFmpeg解码的数据结构

ffmpegDecoder01.png

2.3 FFmpeg解码的流程

ffmpegPlayAudio.png

2.4 API部分说明

FFmpeg其他的功能先不说, 再看看本文的标题. 是的我这篇文章是用它来搞音频的, 解码音频的. 我们这篇文章是播放一个文件(说这句话是相对于网络流来说),

QQ20180806-142933@2x.png

然后请再看一遍这个图, AudioUnit要的是音频采样数据PCM, 我们现在有的是什么? 是一个mp4文件或者一个mp3文件, 是文件! FFmpeg我们用它干嘛? 我们用它扣出PCM数据,然后喂给AudioUnit. 说到头就是解码. 解码就是用的解码流程里的avcodec_decode_audio4

扣PCM喂给AudioUnit, 到底怎么扣?
还是前面那个套路哈, 一句话简单说就是: 不管用FFmpeg解码音频也会视频也好,第一步都是先注册, 第二步就是去拿封装格式上下文AVFormatContext, 第三部用AVFormatContextAVStream,拿到流后第四部用它换解码器上下文AVCodecContext, 然后第五步我们就要用解码器上下文去读取编码数据AVPacket, 最后第七步我们解码编码数据通过avcodec_decode_audio4函数换取PCM裸数据AVFrame

  • AVFormatContext封装格式讲解

关于封装格式的话, 先看代码吧



_avFormatContext = avformat_alloc_context();

int result = avformat_open_input(&_avFormatContext,
                                     [audioFileStr UTF8String],
                                     NULL,
                                     NULL);

 int   result = avformat_find_stream_info(_avFormatContext, NULL);
   

其实这块都不用怎么解释我们相信大家都能看懂

  • AVStream音频流讲解
    先看上面的封装格式那个图. 封装格式由音频编码数据和视频编码数据组成(有的还有字幕数据), 我从网上下来部星爷赌圣, mkv格式的电影, 然后使用ffmpeg命令ffprobe -show_format /Users/codew/Desktop/赌圣.mkv 看看封装格式的组成. 它由7部分组成, 视频编码数据一个Video: h264 (High), 有两个音频编码数据都是Audio: aac (HE-AACv2), 然后四个字幕数据Subtitle: subrip如图
ffmpegDu.png

这些视频呀,音频呀,字幕呀在FFmpeg数据结构里面就是我们说的AVStream, 看见上图中Stream #0:0(chi), Stream #0:1(chi)等等了吗? 这些流是有序号的. 我们要用这个些流我们得找到流对应的序号就像下面这样


_stream_index = av_find_best_stream(_avFormatContext,
                                        AVMEDIA_TYPE_AUDIO,
                                        -1,
                                        -1,
                                        NULL,
                                        0);

我们通过上面的代码拿到了序号, 我们就可以通过序号去拿音频流数据了, 这里是拿音频序号因为本文是研究音频播放的,所以Demo里也只会出现如上拿音频的API视频呀字幕呀本文不会介绍. 下面是通过序号拿音频流


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


  • AVCodecContext解码器上下文

我们要拿流数据AVStream是用来换取解码器和解码器上下文的.为什么有了解码器还要什么解码器上下文?因为我们后面解码用到的函数avcodec_decode_audio4要传的是上下文,第二解码器上下文里面包含了解码器


    // 获得音频流的解码器上下文
    _avCodecContext = audioStream->codec;
    // 根据解码器上下文找到解码器
    AVCodec *avCodec = avcodec_find_decoder(_avCodecContext->codec_id);
    
    // 打开解码器
    result = avcodec_open2(_avCodecContext, avCodec, NULL);

3. 工程Demo大概讲解哈

Demo工程.png

AudioUnit主要逻辑在ST_AudioOutput里面, AVAudioSession使用ST_AudioSession这个封装类

FFmpeg使用在STFFmpegLocalAudioDecoder

然后用到了生产模式消费模式搞了一个线程不间断的生产数据,然后放到队列中,系统快消耗完了就去补货,具体体现在STMediaCacheSTLinkedBlockingQueue

实际上重要的先看懂上面的流程图比较总要, FFmpeg API的使用实际上套路都差不多, 注册找上下文找流找解码器解码....我个人觉得FFmpeg按文理来说我觉得它属于文科.....那有人问了"我最开始应该怎么学?"我的觉得哈买本我不是跟谁谁打广告哈, 书是比较系统性的网上的多的是之言片语少了从头到尾,我这篇也是.第二是FFmpeg源码里的examples, 就好像ffmpeg-3.4.2源码里examples的位置是/ffmpeg-3.4.2/doc/examples, 想学哪个学哪个差不多的功能都有了, 然后就是网上的各种博客了.

我这篇文章是看了<<FFmpeg从入门到精通>>和<<音视频开发进阶指南>>还有雷霄骅博士博客,当然也阅读了些博客, 实际上我这篇文章的Demo也是改写了<<音视频开发进阶指南>>书里的例子, 因为只是Demo嘛多少还有些问题, 希望能帮助你吧. 如果觉得还行记得给我点个赞,表扬我一下,啊哈哈哈哈哈~然后我将大白话iOS音视频继续扯下去?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容