- 关于iOS平台音频的播放我们已经简单了解过了,是了解过简单的了,上一篇章中,播放器需要的PCM的数据,那现在我们就开始讲解音频的解码,就是如何生成PCM数据。
- 音频解码的过程是和视频差不多的,
特别是打开文件和读取流信息这一部分
,所以如果有重复的地方我就跳过了,不明白的小伙伴可以回头看这篇文章。基于iOS平台的最简单的FFmpeg视频播放器(一)
基于iOS平台的最简单的FFmpeg音频播放器(一)
基于iOS平台的最简单的FFmpeg音频播放器(二)
基于iOS平台的最简单的FFmpeg音频播放器(三)
正式开始
正式解码前的准备工作
-
AVFormatContext
是一个FFmpeg
解封装用的结构体,很多函数都用到它作为参数。
+ (void)initialize
{
av_log_set_callback(FFLog);
av_register_all();
}
-
av_register_all()
这个是所有使用FFmpeg
库的时候都需要调用的初始化,可以初始化各种组件和协议等。
1.1 解封装
- (BOOL)openInput:(NSString *)path
{
AVFormatContext * formatCtx = NULL;
formatCtx = avformat_alloc_context();
if (!formatCtx)
{
NSLog(@"打开文件失败");
return NO;
}
if (avformat_open_input(&formatCtx, [path cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL) < 0)
{
if (formatCtx)
{
avformat_free_context(formatCtx);
}
NSLog(@"打开文件失败");
return NO;
}
if (avformat_find_stream_info(formatCtx, NULL) < 0)
{
avformat_close_input(&formatCtx);
NSLog(@"无法获取流信息");
return NO;
}
av_dump_format(formatCtx, 0, [path.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding], false);
_formatCtx = formatCtx;
return YES;
}
- 以上的代码之前有解析过,所以就不重复解释了。
1.2 找到音频流
- (BOOL)findAudioStream {
_audioStream = -1;
for (NSInteger i = 0; i < _formatCtx->nb_streams; i++) {
if (AVMEDIA_TYPE_AUDIO == _formatCtx->streams[i]->codec->codec_type) {
if ([self openAudioStream: i])
break;
}
}
return true;
}
- 遍历已经初始化好的
AVFormatContext
中的流信息,找到解码器媒体类型为AVMEDIA_TYPE_AUDIO
的流,准备初始化解码器。
1.3 初始化音频解码器相关
1.3.1 初始化音频解码器
AVCodecContext *codecCtx = _formatCtx->streams[audioStream]->codec;
SwrContext *swrContext = NULL;
AVCodec *codec = avcodec_find_decoder(codecCtx->codec_id);
if(!codec)
return false;
if (avcodec_open2(codecCtx, codec, NULL) < 0)
return false;
- 以上的代码之前也有解析过,所以就不重复解释了。
1.3.2 判断是否需要重采样
static BOOL audioCodecIsSupported(AVCodecContext *audio)
{
if (audio->sample_fmt == AV_SAMPLE_FMT_S16) {
AieAudioManager * audioManager = [AieAudioManager audioManager];
return (int)audioManager.samplingRate == audio->sample_rate &&
audioManager.numOutputChannels == audio->channels;
}
return NO;
}
-
AV_SAMPLE_FMT_S16
之前有提到过,是播放器需要的一种格式,所以如果解码上下文中采样格式不是AV_SAMPLE_FMT_S16
,或者声道数、采样率和播放器不匹配,那就需要重采样。 - 实际上还有一种方法,就是根据音频的声道数、采样率去调节播放器的参数来播放音频,这种方法我们就先不解释了,以后有空再做深度的讨论。
1.3.3 音频重采样
- 音频重采样算是数字信号处理中的一个单独的模块,拿出来讲三天三夜都讲不完,初学的小伙伴就理解成:
第一次采样的采样率、声道数、采样格式不符合我们要求,所以需要重新采样
(掩饰了我自己也不懂的事实)。
if (!audioCodecIsSupported(codecCtx)) {
AieAudioManager * audioManager = [AieAudioManager audioManager];
swrContext = swr_alloc_set_opts(NULL,
av_get_default_channel_layout(audioManager.numOutputChannels),
AV_SAMPLE_FMT_S16,
audioManager.samplingRate,
av_get_default_channel_layout(codecCtx->channels),
codecCtx->sample_fmt,
codecCtx->sample_rate,
0,
NULL);
if (!swrContext ||
swr_init(swrContext)) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return false;
}
}
-
SwrContext
是重采样相关的结构体,但是这个结构体FFmpeg
是没有开放给我们的,我们并不能看到它的参数,只能通过一系列的方法来操作它。 -
swr_alloc_set_opts()
这个方法就是设置SwrContext
参数的函数。
1. 第一个参数:如果你想重新设置一个SwrContext
的参数,那就直接传入这个SwrContext
,否则传NULL
,它会重新生成一个新的SwrContext
,并作为返回值返回。
2. 第二个参数:重采样后的通道数,但是这里需要使用av_get_default_channel_layout()
函数包裹一下,获取默认的通道数,其实如果直接传通道数也没有太大的影响。
3. 第三个参数:重采样后的采样格式,这里肯定是AV_SAMPLE_FMT_S16
,我们需要的就是这种数据。
4. 第四个参数:重采样后的采样率。
5. 第五个参数:重采样前的通道数。
6. 第六个参数:重采样前的采样格式。
7. 第七个参数:重采样前的采样率。
8. 最后两个参数没什么好解释的,直接传0
和NULL
就好了。 -
swr_init()
设置了参数之后,才可以进行SwrContext
的初始化。
1.3.4 设置帧率等基本操作
_audioFrame = av_frame_alloc();
if (!_audioFrame) {
if (swrContext)
swr_free(&swrContext);
avcodec_close(codecCtx);
return false;
}
_audioStream = audioStream;
_audioCodecCtx = codecCtx;
_swrContext = swrContext;
AVStream *st = _formatCtx->streams[_audioStream];
avStreamFPSTimeBase(st, 0.025, 0, &_audioTimeBase);
- 上面的模块依然是和视频解码的部分是重复的,不懂的小伙伴可以去看视频解码的文章。
2. 开始解码
- 解码过程之前也说了,音视频的全都一个样,看之前的文章。(突然觉得这篇文章全是废话)。
- (NSArray *)decodeFrames:(CGFloat)minDuration
{
if (_audioStream == -1) {
return nil;
}
NSMutableArray * result = [NSMutableArray array];
AVPacket packet;
CGFloat decodedDuration = 0;
BOOL finished = NO;
while (!finished) {
if (av_read_frame(_formatCtx, &packet) < 0) {
NSLog(@"读取Frame失败");
break;
}
if (packet.stream_index == _audioStream) {
int pktSize = packet.size;
while (pktSize > 0) {
int gotframe = 0;
int len = avcodec_decode_audio4(_audioCodecCtx,
_audioFrame,
&gotframe,
&packet);
if (len < 0) {
break;
}
if (gotframe) {
AieAudioFrame * frame = [self handleAudioFrame];
frame.type = AieFrameTypeAudio;
if (frame) {
[result addObject:frame];
_position = frame.position;
decodedDuration += frame.duration;
NSLog(@"---当前时间:%f, 持续时间: %f , 总时间:%f", _position, frame.duration, decodedDuration);
if (decodedDuration > minDuration)
finished = YES;
}
}
if (0 == len)
break;
pktSize -= len;
}
}
av_free_packet(&packet);
}
return result;
}
- 唯一不同的是音频解码调用的函数不同,是
avcodec_decode_audio4()
。
3. 处理解码后的数据
- 解码后的数据还是和之前解码结构体中的类型是一样的,重采样就是在解码后再进行的,之前只是定义重采样的结构体而已,并没有进行真正的重采样。
3.1 真正的重采样
if (_swrContext) {
const NSUInteger ratio = MAX(1, audioManager.samplingRate / _audioCodecCtx->sample_rate) *
MAX(1, audioManager.numOutputChannels / _audioCodecCtx->channels) * 2;
const int bufSize = av_samples_get_buffer_size(NULL,
audioManager.numOutputChannels,
_audioFrame->nb_samples * ratio,
AV_SAMPLE_FMT_S16,
1);
if (!_swrBuffer || _swrBufferSize < bufSize) {
_swrBufferSize = bufSize;
_swrBuffer = realloc(_swrBuffer, _swrBufferSize);
}
Byte *outbuf[2] = { _swrBuffer, 0 };
numFrames = swr_convert(_swrContext,
outbuf,
_audioFrame->nb_samples * ratio,
(const uint8_t **)_audioFrame->data,
_audioFrame->nb_samples);
if (numFrames < 0) {
return nil;
}
audioData = _swrBuffer;
} else {
if (_audioCodecCtx->sample_fmt != AV_SAMPLE_FMT_S16) {
NSAssert(false, @"bucheck, audio format is invalid");
return nil;
}
audioData = _audioFrame->data[0];
numFrames = _audioFrame->nb_samples;
}
-
ratio
这个值的计算其实我也不是很清楚,我的理解是因为iOS都是单声道的原因,所以最后需要乘以2。 -
av_samples_get_buffer_size()
计算给定音频参数所需要的缓冲区大小。
1. 第一个参数:本来应该传入一个linesize的指针(数组),但是解码后的_audioFrame-> linesize的大小不一定准确,所以需要重新计算,所以这里一般传NULL
就好了。
2. 第二个参数:通道数。
3. 第三个参数:每一帧的采样率,我们平时所说的音频采样率(44100)指的是:每秒钟采样的次数。
4. 第四个参数:采样的格式,肯定是AV_SAMPLE_FMT_S16
。
5. 第五个参数:是否对齐,0
是默认对齐,1
是不对齐,其实这里传哪个都差不多,没什么关系。 -
_swrContext()
这个函数就是进行重采样的方法。
1. 第一个参数:SwrContext
重采样相关的结构体,一定是要设置过参数的。
2. 第二个参数:重采样之后的数据,作为参数的时候肯定是为空,重采样结束之后,里面就是重采样之后的数据了。
3. 第三个参数:重采样之后每帧的采样率。
4. 第四个参数:重采样之前的数据。
5. 第五个参数:重采样之前每帧的采样率。
3.2 把数据转化成模型
const NSUInteger numElements = numFrames * numChannels;
NSMutableData *data = [NSMutableData dataWithLength:numElements * sizeof(float)];
float scale = 1.0 / (float)INT16_MAX ;
vDSP_vflt16((SInt16 *)audioData, 1, data.mutableBytes, 1, numElements);
vDSP_vsmul(data.mutableBytes, 1, &scale, data.mutableBytes, 1, numElements);
AieAudioFrame *frame = [[AieAudioFrame alloc] init];
frame.position = av_frame_get_best_effort_timestamp(_audioFrame) * _audioTimeBase;
frame.duration = av_frame_get_pkt_duration(_audioFrame) * _audioTimeBase;
frame.samples = data;
- 这里还是使用了加速框架进行傅里叶转换,函数的作用还是和上篇文章说的一样,但是播放之前转化了一次,这里又转化了一次,我也不是很清楚,知道的小伙伴可以评论讨论下。
-
av_frame_get_best_effort_timestamp()
这个函数和视频解码的时候一样,以流中的时间戳为基础预估这一帧的时间戳,不过最后的结果是要乘以基时。 -
av_frame_get_pkt_duration()
是获取这一帧数据持续的时间。
结尾
这一篇章,我自己还是有几个不理解的地方,希望有知道的大佬和我讨论下,万分感谢。
- 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
谢谢阅读。