基于iOS平台的最简单的FFmpeg音频播放器(一)
2018-05-23 本文已影响458人
Aiewing
-
之前的
FFmpeg
的视频播放器的文章,肯定已经可以让初学者在视频的解码过程有一定的了解了,现在我们来深度解析一下音频的解码和播放,实质上音频和视频的解码过程也是是极为雷同的(这里说的是针对FFmpeg
的代码层次,不是实际的解码过程)。 -
在学习音频的解码和播放之前,小伙伴们理应对音频的支持有所了解,才可以更好的理解音频的解码和播放,建议可以去看雷神的博客,其实我也不知道他的文章是否有对音频的详细讲解,已经很久没有看过了,反正就是
丢你雷总
,看了总比不看好。 -
音频的解码和播放,我们还是分为三个章节来讲解,但是有所不同的是,我们这次反过来讲,我们先讲解音频的播放,然后再讲解音频的解码。
基于iOS平台的最简单的FFmpeg音频播放器(一)
基于iOS平台的最简单的FFmpeg音频播放器(二)
基于iOS平台的最简单的FFmpeg音频播放器(三)
正式开始
音频的基本特性
- 本来说好不讲音频的基本知识的,但是还是忍不住普及一下。所谓的手机上的声音就是声卡把数字信号转化成模型信号,然后通过线圈和磁铁产生不同的震动,这就是产生了我们平时听见手机扬声器的声音了。
- 想要播放一个音频,我们需要的是什么数据,我目前遇到的都是
AV_SAMPLE_FMT_S16
格式的数据,解释一下就是非平面的16位的PCM格式的原数据
,这个是iPhone手机使用原生的录制功能出来的音频数据,不理解没关系,我也不理解,不影响写代码
。
1.初始化音频播放器
- 上面好像有奇怪的东西出现,说了一些没什么卵用的东西,肯定是我被另一个人格操控了。
1.1 创建音频组件描述
AudioComponentDescription description = {0};
description.componentType = kAudioUnitType_Output;
description.componentSubType = kAudioUnitSubType_RemoteIO;
description.componentManufacturer = kAudioUnitManufacturer_Apple;
-
AudioComponentDescription
是一个用来描述音频组建的结构体。 -
componentType
是一个音频组件通用的独特的四字节标识码,我们这里是要播放音频所以选择的是kAudioUnitType_Output
,里面还有很多类型,反正我是没用到过。 -
componentSubType
是根据componentType
设置的类型,iOS平台kAudioUnitSubType_RemoteIO
,如果是iOS和OSX平台通用的是kAudioUnitSubType_VoiceProcessingIO
。 -
componentManufacturer
是厂商的身份验证,这个不用考虑了,我选kAudioUnitManufacturer_Apple
。
1.2 创建音频输出单元
AudioComponent component = AudioComponentFindNext(NULL, &description);
status = AudioComponentInstanceNew(component, &_audioUnit);
if (status != noErr) {
NSLog(@"无法创建音频输出单元");
return false;
}
-
AudioUnit.framework
这个库提供DSP数字信号处理相关的插件,包括编解码,混音,音频均衡等。 -
AudioComponentFindNext()
用于寻找最接近匹配的音频组件。 -
AudioComponentInstanceNew()
创建一个音频组件的实例,就是我们需要的音频输出单元。
1.3 获取设备音频的基本信息
// 获取硬件的输出信息
size = sizeof(AudioStreamBasicDescription);
status = AudioUnitGetProperty(_audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
0,
&_outputFormat,
&size);
if (status != noErr) {
NSLog(@"无法获取硬件的输出流格式");
return false;
}
_numBytesPerSample = _outputFormat.mBitsPerChannel / 8;
_numOutputChannels = _outputFormat.mChannelsPerFrame;
-
AudioStreamBasicDescription
是描述音频流数据基本属性的全部信息的结构体。 -
AudioUnitGetProperty()
作用是获取Audio Unit
属性的函数,常用的属性有:
-kAudioUnitProperty_StreamFormat
(Audio unit
输入输出流的数据格式)
-kAudioOutputUnitProperty_EnableIO
(开启或者禁用I/O)
-kAudioUnitProperty_SetRenderCallback
(设置Audio unit
的播放回调,下面会用到) -
_numBytesPerSample = _outputFormat.mBitsPerChannel / 8;
是获取每一个采样数据的字节数,固定就是单个通道的采样数据的字节数除以8. -
_numOutputChannels = _outputFormat.mChannelsPerFrame;
如果是1,那就是单声道,如果是2,那就是双声道。
1.4 设置Audio Unit回调
// 设置回调
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = renderCallback;
callbackStruct.inputProcRefCon = (__bridge void *)(self);
status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
0,
&callbackStruct,
sizeof(callbackStruct));
if (status != noErr) {
NSLog(@"无法设置音频输出单元的回调");
return false;
}
-
AURenderCallbackStruct
是很重要的一个结构体,控制音频输入输出回调,这里当函数回调时,我们就要把准备播放的数据加入到播放列表中。 - 这边的回调函数是固定写法,下面会花一点时间单独讲解。
1.5 初始化Audio Unit
// 初始化音频输出单元
status = AudioUnitInitialize(_audioUnit);
if (status != noErr) {
NSLog(@"无法初始化音频输出单元");
return false;
}
-
Audio Unit
一旦被初始化成功之后,他的输入输出的格式就都是有效的,而且不能改变内存分配。
2.启动Audio Unit
// 启动音频输出单元
OSStatus status = AudioOutputUnitStart(_audioUnit);
if (status == noErr) {
_playing = true;
} else {
_playing = false;
}
- 启动了
Audio Unit
,放入音频数据,音频就开始播放了,苹果自带的音频播放器就这么简单。
3.音频播放回调解析
- 前面说过,音频播放回调会单独拿出来讲,因为不是所有的数据都是可以直接播放的,还需要做一定的处理。
#pragma mark - CallBack
static OSStatus renderCallback (void *inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inOutputBusNumber,
UInt32 inNumberFrames,
AudioBufferList * ioData)
{
AieAudioManager * aam = (__bridge AieAudioManager *)inRefCon;
return [aam renderFrames:inNumberFrames ioData:ioData];
}
- 这个函数是
Audio Unit
渲染通知API,也是渲染输出回调。 -
inRefCon
第一个参数就是我们之前设置函数函数回调的时候,传入的inputProcRefCon
。 -
ioActionFlags
是用来描述上下文的标记。 -
inTimeStamp
是音频渲染的时间戳。 -
inOutputBusNumber
是与音频渲染相关的总线号。 -
inNumberFrames
是ioData
中音频数据采样帧的数量。 -
ioData
是一个列表,用来存放准备播放的数据。
- (BOOL)renderFrames:(UInt32)numFrames ioData:(AudioBufferList *)ioData
{
// 第一步
for (int iBuffer = 0; iBuffer < ioData->mNumberBuffers; iBuffer++) {
memset(ioData->mBuffers[iBuffer].mData, 0, ioData->mBuffers[iBuffer].mDataByteSize);
}
if (_playing && _outputBlock) {
// 第二步
_outputBlock(_outData, numFrames, _numOutputChannels);
if (_numBytesPerSample == 2) {
// 第三步
float scale = (float)INT16_MAX;
vDSP_vsmul(_outData,
1,
&scale,
_outData,
1,
numFrames * _numOutputChannels);
// 第四步
for (int iBuffer = 0; iBuffer < ioData->mNumberBuffers; ++iBuffer) {
int thisNumChannels = ioData->mBuffers[iBuffer].mNumberChannels;
for (int iChannel = 0; iChannel < thisNumChannels; ++iChannel) {
vDSP_vfix16(_outData+iChannel,
_numOutputChannels,
(SInt16 *)ioData->mBuffers[iBuffer].mData+iChannel,
thisNumChannels, numFrames);
}
}
}
}
return noErr;
}
1. 第一步是初始化ioData
,这个算是属于一个C语言范畴的函数,就是把ioData
列表中所有的数据都用0
来填充。
2. 第二步是使用block
从外界,把需要的数据(实际上就是解码后的PCM数据)填充到_outData
中来。
3. 第三步是利用vDSP_vsmul()
对数据做了一个乘法的运算。
4. 第四步是对所有ioData
数据,根据不同的声道,使用vDSP_vfix16()
将非交错的16位带符号整数转化成单精度浮点型(因为苹果设备都是每个采样点16bit量化),如果是每个采样点32bit量化,那就需要使用vDSP_vflt32()
。
- 做完这些操作之后,音频就可以进行播放了。
- 上面提到的
sDSP
函数都是属于Accelerate.framework
加速处理框架,其中包括向量和矩阵算法
,傅里叶变换
,双二次滤波
等。
4.拓展方法
- 如果说一个最简单的音频播放器,那么上面就已经满足了,但是如果作为一个功能全面的应用,那还是远远不足的,下面我就简单的描述下。
-
Audio Session
可以帮我们处理各种系统逻辑,如果你是一个以语音为主的应用,设置kAudioSessionProperty_AudioCategory
属性可以让应用不会随着静音键和屏幕关闭而静音,可以在后台播放。 - 如果你的应用需要播放声音又需要录音,那就设置这个属性
kAudioSessionCategory_PlayAndRecord
。 - 如果你需要监听耳机的插入或者拔出,那你需要监听
kAudioSessionProperty_AudioRouteChange
这个属性,作用是监听音频路线是否改变。 - 如果你想让应用主动获取系统音量,那就监听
kAudioSessionProperty_CurrentHardwareOutputVolume
这个属性。
结尾
- 代码会在整个播放器讲完之后再给出完整的。
- 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
- 谢谢阅读