- 在前面两个篇章中,已经把解码和播放两个重要的模块讲完了,剩下的只有一些处理中间逻辑的代码了,看来这一篇是又要水了,但是也是重要要的处理部分。
- 音频解码逻辑和之前的视频解码逻辑一样样的,下面我们可能只会列举出一些不相同的部分。
基于iOS平台的最简单的FFmpeg音频播放器(一)
基于iOS平台的最简单的FFmpeg音频播放器(二)
基于iOS平台的最简单的FFmpeg音频播放器(三)
正式开始
1. 初始化音频播放器
AieAudioManager * audioManager = [AieAudioManager audioManager];
[audioManager activateAudioSession];
- 音频播放器理论上应该先初始化,获取和设置硬件参数。
2.初始化解码库
- (void)start
{
_path = [[NSBundle mainBundle] pathForResource:@"薛之谦 - 摩天大楼" ofType:@"m4a"];
__weak AudioPlayController * weakSelf = self;
AieDecoder * decoder = [[AieDecoder alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSError * error = nil;
[decoder openFile:_path error:&error];
__strong AudioPlayController * strongSelf = weakSelf;
if (strongSelf)
{
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf setMovieDecoder:decoder];
});
}
});
}
- 初始化解码库,然后使用
FFmpeg
相关的函数去打开文件,解析音频文件中的流信息等。
3.开始解码,并打开音频
- (void)play
{
// 解码音频 并把音频存储到_audioFrames
[self asyncDecodeFrames];
// 打开音频
[self enableAudio:YES];
}
- (void)enableAudio: (BOOL) on
{
AieAudioManager * audioManager = [AieAudioManager audioManager];
if (on) {
audioManager.outputBlock = ^(float *outData, UInt32 numFrames, UInt32 numChannels) {
[self audioCallbackFillData: outData numFrames:numFrames numChannels:numChannels];
};
[audioManager play];
}
}
-
asyncDecodeFrames
这个函数就就不说了,和视频解码的时候一毛一样的。 -
AieAudioManager
是一个单例类,所以重复获取都是同一个对象,相比Kxmovie
的音频播放类,我把这个类简化了许多,毕竟是最简单的音频播放器。 -
audioManager.outputBlock
这个回调就是用来接收即将解码的数据的。
4.解码成功,并返回音频数据
- (void)asyncDecodeFrames
{
__weak AudioPlayController * weakSelf = self;
__weak AieDecoder * weakDecoder = _decoder;
dispatch_async(_dispatchQueue, ^{
// 当已经解码的视频总时间大于_maxBufferedDuration 停止解码
BOOL good = YES;
while (good) {
good = NO;
@autoreleasepool {
__strong AieDecoder * strongDecoder = weakDecoder;
if (strongDecoder) {
NSArray * frames = [strongDecoder decodeFrames:0.1];
if (frames.count) {
__strong AudioPlayController * strongSelf = weakSelf;
if (strongSelf) {
good = [strongSelf addFrames:frames];
}
}
}
}
}
});
}
- 这一部分是和视频解码的流程是一样的,还是不做解释了
5.缓存解码后的音频数据
- (BOOL) addFrames:(NSArray *)frames
{
@synchronized (_audioFrames)
{
for (AieFrame * frame in frames)
{
if (frame.type == AieFrameTypeAudio)
{
[_audioFrames addObject:frame];
_bufferedDuration += frame.duration;
}
}
}
return _bufferedDuration < _maxBufferedDuration;
}
- 缓存解码后的音频数据,并计算出缓存中音频数据的总时间,用来合理的控制解码速度。
6.填充将要解码的数据
- (void) audioCallbackFillData: (float *) outData
numFrames: (UInt32) numFrames
numChannels: (UInt32) numChannels
- 这个函数是本文的重点,是一个从音频播放器回调出来的函数,然后又使用block回调到这个类。
-numFrames
是音频数据采样帧的数量,这个之前有说过。 -
numChannels
是音频的通道数。
6.1 获取音频数据
NSUInteger count = _audioFrames.count;
if (count > 0) {
AieAudioFrame *frame = _audioFrames[0];
[_audioFrames removeObjectAtIndex:0];
_moviePosition = frame.position;
_bufferedDuration -= frame.duration;
_currentAudioFramePos = 0;
_currentAudioFrame = frame.samples;
}
if (!count || !(_bufferedDuration > _minBufferedDuration)) {
[self asyncDecodeFrames];
}
- 以上代码的主要作用是从
_audioFrames
缓存区中逐个取出数据,然后记录当前的播放位置和时间,当缓存区没有数据或者是缓冲时间少于最小的时间的时候,就继续解码。
6.1 填充数据
if (_currentAudioFrame) {
const void *bytes = (Byte *)_currentAudioFrame.bytes + _currentAudioFramePos;
const NSUInteger bytesLeft = (_currentAudioFrame.length - _currentAudioFramePos);
const NSUInteger frameSizeOf = numChannels * sizeof(float);
const NSUInteger bytesToCopy = MIN(numFrames * frameSizeOf, bytesLeft);
const NSUInteger framesToCopy = bytesToCopy / frameSizeOf;
memcpy(outData, bytes, bytesToCopy);
numFrames -= framesToCopy;
outData += framesToCopy * numChannels;
if (bytesToCopy < bytesLeft)
_currentAudioFramePos += bytesToCopy;
else
_currentAudioFrame = nil;
} else {
memset(outData, 0, numFrames * numChannels * sizeof(float));
//LoggerStream(1, @"silence audio");
break;
}
- 如果
_currentAudioFrame
中存在数据,那就一帧一帧数据的存进outData
中,如果_currentAudioFrame
中的音频数据大于一帧,那就分为多次存。 - 从这个
bytesToCopy
的计算方式可以看出,numFrames * numChannels * sizeof(float)
就是一帧的采样数、通道数和一次采样点大小的乘积就是实际上总的一帧音频数据的大小。 - 如果
_currentAudioFrame
中不存在数据,那就直接存0
。
结尾
- 到这里我们关于最简单的音频播放器的内容就全部结束了。
- 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
- 谢谢阅读