基于iOS平台的最简单的FFmpeg音频播放器(二)

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

推荐阅读更多精彩内容