大白话iOS音视频-01-音频播放(FFMpeg+AudioUn
前言瞎扯
实际关于利用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能用的静态库文件如图
看看我这个文章,如果你本地没有编译好的.
===>编译iOS能用的FFmpeg静态库
往下就是具体逻辑讲解了, 默认你懂了关于音频的基础知识和已经编译好iOS能用的静态库了哈, 那啥要不再看看
===>编译iOS能用的FFmpeg静态库
1.AudioUnit
1.1大概原理闲扯
啥也不说. 看看一幅图.
AudioUnitJG.png嗯嗯看看图,AudioUnit在下去就是硬件了.用它处理音视频数据确实略微"复杂"."复杂"的话功能就会有点骚.
AudioUnit 就一个小孩, 需要一直喂东西.我要做的就是不断喂他东西.
....或者说AudioUnit就是一台机器,它生产的产品是声音, 我们要做的就是不断的给他填原料
, 本篇文章就当他是打米机
好了, FFmpeg
就是水稻收割机.
如上图水稻收割机
(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
的入口出口等信息, 最后连接.
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);
AVAudioSession
和 Audio Session
一个是类,一个是一个函数,使用起来还是很不同的, 我们这里用前者. 我们将用一个包装类来使用AVAudioSession, 下面是具体介绍
- 1.获取AVAudioSession实例
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
- 2.设置硬件能力
我们要做什么? 看我的标题,我们只要播放声音,我们想要iPhone手机播放声音.然后我们设置AVAudioSessionCategoryPlayback
, 如果我们要手机采集又播放就是AVAudioSessionCategoryPlayAndRecord
[audioSession setCategory:AVAudioSessionCategoryPlayback];
- 设置I/O的Buffer,
Buffer
越小则说明延迟越低
- 设置I/O的Buffer,
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还没半毛钱关系的哈, 但是再看一下这个图
AudioUnit
下面就是驱动和硬件了意思是它是跟硬件和驱动直接大交道的, 所以使用AudioUnit之前必须要创建一个会话管理获取硬件相关信息.
虽然没有用到AudioUnit, 但是却对其有很大的影响,代码不多就⑤步~
1.3.2 创建AudioUnit
实际上AudioUnit
是一个大类名称,看图
还是打米机哈,不好意思哈我真的觉得这家伙和打米机好像([捂脸] 哈哈哈哈~), 如图打米机有很多种型号,有的打米机不只有"打米"的功能还有将小麦加工成面粉呢(并没有真的见过那种机器~瞎扯的).
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
的入口出口等信息, 最后连接.
- 第一步就是拿到
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
上面我们的到了AUNode
和AUGraph
, 现在我们可以通过他们召唤出真正的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 0
和Element 1
, 其中Element 0
控制输出端, Element 1
控制输入端. 同时每个Element 又分为Input Scope
和Output Scope
. 看图中APP和Element 1, Element 0
的连线, 如果我们只是想播放声音就将我们的APP与Element 0
的Input Scope
连接起来, 如果我们只是想要通过麦克风录音我们就将我们的APP与Element 1
的Output Scope
连接起来, 所谓的"连接"代码里的体现就是设置两个回调函数
本文是干嘛的, 就音频播放. 所以我们只是想播放声音就将我们的APP与
Element 0的
Input 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];
}
这个函数不是乱写的, 我们点击结构体AURenderCallbackStruct
的inputProc
可以看到函数原型如下. 我们要做的是实现该函数, 函数名由我们自己定义.我讲函数名定义为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小节是相关介绍
2.1 FFmpeg数据结构简介
- AVFormatContext
封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装 格式相关信息。
- AVInputFormat
每种封装格式(例如FLV, MKV, MP4, AVI)对应一个该结构体。
-
AVStream
视频文件中每个视频(音频)流对应一个该结构体。 -
AVCodecContext
编码器上下文结构体,保存了视频(音频)编解码相关信息。 -
AVCodec
每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体。 -
AVPacket
存储一帧压缩编码数据。 -
AVFrame
存储一帧解码后像素(采样)数据
2.2 FFmpeg解码的数据结构
ffmpegDecoder01.png2.3 FFmpeg解码的流程
ffmpegPlayAudio.png2.4 API部分说明
FFmpeg其他的功能先不说, 再看看本文的标题. 是的我这篇文章是用它来搞音频的, 解码音频的. 我们这篇文章是播放一个文件(说这句话是相对于网络流来说),
QQ20180806-142933@2x.png然后请再看一遍这个图, AudioUnit
要的是音频采样数据PCM
, 我们现在有的是什么? 是一个mp4文件或者一个mp3文件, 是文件! FFmpeg
我们用它干嘛? 我们用它扣出PCM数据,然后喂给AudioUnit
. 说到头就是解码. 解码就是用的解码流程里的avcodec_decode_audio4
扣PCM喂给AudioUnit
, 到底怎么扣?
还是前面那个套路哈, 一句话简单说就是: 不管用FFmpeg
解码音频也会视频也好,第一步都是先注册, 第二步就是去拿封装格式上下文AVFormatContext
, 第三部用AVFormatContext
换AVStream
,拿到流后第四部用它换解码器上下文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
如图
这些视频呀,音频呀,字幕呀在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工程.pngAudioUnit主要逻辑在ST_AudioOutput
里面, AVAudioSession使用ST_AudioSession
这个封装类
FFmpeg使用在STFFmpegLocalAudioDecoder
然后用到了生产模式消费模式搞了一个线程不间断的生产数据,然后放到队列中,系统快消耗完了就去补货,具体体现在STMediaCache
和STLinkedBlockingQueue
后
实际上重要的先看懂上面的流程图比较总要, FFmpeg API的使用实际上套路都差不多, 注册找上下文找流找解码器解码....我个人觉得FFmpeg按文理来说我觉得它属于文科.....那有人问了"我最开始应该怎么学?"我的觉得哈买本我不是跟谁谁打广告哈, 书是比较系统性的网上的多的是之言片语少了从头到尾,我这篇也是.第二是FFmpeg源码里的examples
, 就好像ffmpeg-3.4.2源码里examples
的位置是/ffmpeg-3.4.2/doc/examples
, 想学哪个学哪个差不多的功能都有了, 然后就是网上的各种博客了.
我这篇文章是看了<<FFmpeg从入门到精通>>和<<音视频开发进阶指南>>还有雷霄骅博士博客,当然也阅读了些博客, 实际上我这篇文章的Demo也是改写了<<音视频开发进阶指南>>书里的例子, 因为只是Demo嘛多少还有些问题, 希望能帮助你吧. 如果觉得还行记得给我点个赞,表扬我一下,啊哈哈哈哈哈~然后我将大白话iOS音视频继续扯下去?