基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(二)

  • 關(guān)于iOS平臺(tái)音頻的播放我們已經(jīng)簡(jiǎn)單了解過了棒旗,是了解過簡(jiǎn)單的了誓篱,上一篇章中,播放器需要的PCM的數(shù)據(jù)笼沥,那現(xiàn)在我們就開始講解音頻的解碼蚪燕,就是如何生成PCM數(shù)據(jù)招狸。
  • 音頻解碼的過程是和視頻差不多的,特別是打開文件和讀取流信息這一部分邻薯,所以如果有重復(fù)的地方我就跳過了裙戏,不明白的小伙伴可以回頭看這篇文章。基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg視頻播放器(一)

基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(一)
基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(二)
基于iOS平臺(tái)的最簡(jiǎn)單的FFmpeg音頻播放器(三)

正式開始

正式解碼前的準(zhǔn)備工作

  • AVFormatContext 是一個(gè)FFmpeg解封裝用的結(jié)構(gòu)體厕诡,很多函數(shù)都用到它作為參數(shù)累榜。
+ (void)initialize
{
    av_log_set_callback(FFLog);
    av_register_all();
}
  • av_register_all()這個(gè)是所有使用FFmpeg庫(kù)的時(shí)候都需要調(diào)用的初始化,可以初始化各種組件和協(xié)議等灵嫌。

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;
}
  • 以上的代碼之前有解析過壹罚,所以就不重復(fù)解釋了。

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;
}
  • 遍歷已經(jīng)初始化好的AVFormatContext中的流信息寿羞,找到解碼器媒體類型為AVMEDIA_TYPE_AUDIO的流猖凛,準(zhǔn)備初始化解碼器。

1.3 初始化音頻解碼器相關(guān)

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;
  • 以上的代碼之前也有解析過绪穆,所以就不重復(fù)解釋了辨泳。

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菠红,或者聲道數(shù)、采樣率和播放器不匹配难菌,那就需要重采樣试溯。
  • 實(shí)際上還有一種方法,就是根據(jù)音頻的聲道數(shù)郊酒、采樣率去調(diào)節(jié)播放器的參數(shù)來播放音頻遇绞,這種方法我們就先不解釋了,以后有空再做深度的討論燎窘。

1.3.3 音頻重采樣

  • 音頻重采樣算是數(shù)字信號(hào)處理中的一個(gè)單獨(dú)的模塊摹闽,拿出來講三天三夜都講不完,初學(xué)的小伙伴就理解成:第一次采樣的采樣率荠耽、聲道數(shù)钩骇、采樣格式不符合我們要求,所以需要重新采樣(掩飾了我自己也不懂的事實(shí))铝量。
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是重采樣相關(guān)的結(jié)構(gòu)體,但是這個(gè)結(jié)構(gòu)體FFmpeg是沒有開放給我們的银亲,我們并不能看到它的參數(shù)慢叨,只能通過一系列的方法來操作它。
  • swr_alloc_set_opts()這個(gè)方法就是設(shè)置SwrContext參數(shù)的函數(shù)务蝠。
    1. 第一個(gè)參數(shù):如果你想重新設(shè)置一個(gè)SwrContext的參數(shù)拍谐,那就直接傳入這個(gè)SwrContext,否則傳NULL,它會(huì)重新生成一個(gè)新的SwrContext轩拨,并作為返回值返回践瓷。
    2. 第二個(gè)參數(shù):重采樣后的通道數(shù),但是這里需要使用av_get_default_channel_layout()函數(shù)包裹一下亡蓉,獲取默認(rèn)的通道數(shù)晕翠,其實(shí)如果直接傳通道數(shù)也沒有太大的影響。
    3. 第三個(gè)參數(shù):重采樣后的采樣格式砍濒,這里肯定是AV_SAMPLE_FMT_S16淋肾,我們需要的就是這種數(shù)據(jù)。
    4. 第四個(gè)參數(shù):重采樣后的采樣率爸邢。
    5. 第五個(gè)參數(shù):重采樣前的通道數(shù)樊卓。
    6. 第六個(gè)參數(shù):重采樣前的采樣格式。
    7. 第七個(gè)參數(shù):重采樣前的采樣率杠河。
    8. 最后兩個(gè)參數(shù)沒什么好解釋的碌尔,直接傳0NULL就好了。
  • swr_init()設(shè)置了參數(shù)之后券敌,才可以進(jìn)行SwrContext的初始化七扰。

1.3.4 設(shè)置幀率等基本操作

_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);
  • 上面的模塊依然是和視頻解碼的部分是重復(fù)的,不懂的小伙伴可以去看視頻解碼的文章陪白。

2. 開始解碼

  • 解碼過程之前也說了颈走,音視頻的全都一個(gè)樣,看之前的文章咱士。(突然覺得這篇文章全是廢話)立由。
- (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(@"---當(dāng)前時(shí)間:%f, 持續(xù)時(shí)間: %f , 總時(shí)間:%f", _position, frame.duration, decodedDuration);
                        if (decodedDuration > minDuration)
                            finished = YES;
                    }
                }
                
                if (0 == len)
                    break;
                
                pktSize -= len;
            }
        }
        
        av_free_packet(&packet);
    }
    
    return result;
}
  • 唯一不同的是音頻解碼調(diào)用的函數(shù)不同,是avcodec_decode_audio4()序厉。

3. 處理解碼后的數(shù)據(jù)

  • 解碼后的數(shù)據(jù)還是和之前解碼結(jié)構(gòu)體中的類型是一樣的锐膜,重采樣就是在解碼后再進(jìn)行的,之前只是定義重采樣的結(jié)構(gòu)體而已弛房,并沒有進(jìn)行真正的重采樣道盏。

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這個(gè)值的計(jì)算其實(shí)我也不是很清楚,我的理解是因?yàn)閕OS都是單聲道的原因文捶,所以最后需要乘以2荷逞。
  • av_samples_get_buffer_size()計(jì)算給定音頻參數(shù)所需要的緩沖區(qū)大小。
    1. 第一個(gè)參數(shù):本來應(yīng)該傳入一個(gè)linesize的指針(數(shù)組)粹排,但是解碼后的_audioFrame-> linesize的大小不一定準(zhǔn)確种远,所以需要重新計(jì)算,所以這里一般傳NULL就好了顽耳。
    2. 第二個(gè)參數(shù):通道數(shù)坠敷。
    3. 第三個(gè)參數(shù):每一幀的采樣率妙同,我們平時(shí)所說的音頻采樣率(44100)指的是:每秒鐘采樣的次數(shù)。
    4. 第四個(gè)參數(shù):采樣的格式膝迎,肯定是AV_SAMPLE_FMT_S16粥帚。
    5. 第五個(gè)參數(shù):是否對(duì)齊,0是默認(rèn)對(duì)齊限次,1是不對(duì)齊芒涡,其實(shí)這里傳哪個(gè)都差不多,沒什么關(guān)系掂恕。
  • _swrContext()這個(gè)函數(shù)就是進(jìn)行重采樣的方法拖陆。
    1. 第一個(gè)參數(shù):SwrContext重采樣相關(guān)的結(jié)構(gòu)體,一定是要設(shè)置過參數(shù)的懊亡。
    2. 第二個(gè)參數(shù):重采樣之后的數(shù)據(jù)依啰,作為參數(shù)的時(shí)候肯定是為空,重采樣結(jié)束之后店枣,里面就是重采樣之后的數(shù)據(jù)了速警。
    3. 第三個(gè)參數(shù):重采樣之后每幀的采樣率。
    4. 第四個(gè)參數(shù):重采樣之前的數(shù)據(jù)鸯两。
    5. 第五個(gè)參數(shù):重采樣之前每幀的采樣率闷旧。

3.2 把數(shù)據(jù)轉(zhuǎn)化成模型

    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;
  • 這里還是使用了加速框架進(jìn)行傅里葉轉(zhuǎn)換,函數(shù)的作用還是和上篇文章說的一樣钧唐,但是播放之前轉(zhuǎn)化了一次忙灼,這里又轉(zhuǎn)化了一次,我也不是很清楚钝侠,知道的小伙伴可以評(píng)論討論下该园。
  • av_frame_get_best_effort_timestamp()這個(gè)函數(shù)和視頻解碼的時(shí)候一樣,以流中的時(shí)間戳為基礎(chǔ)預(yù)估這一幀的時(shí)間戳帅韧,不過最后的結(jié)果是要乘以基時(shí)里初。
  • av_frame_get_pkt_duration()是獲取這一幀數(shù)據(jù)持續(xù)的時(shí)間。

結(jié)尾

這一篇章忽舟,我自己還是有幾個(gè)不理解的地方双妨,希望有知道的大佬和我討論下,萬(wàn)分感謝叮阅。

  • 由于放了FFmpeg庫(kù)刁品,所以Demo會(huì)很大,下載的時(shí)候比較費(fèi)時(shí)帘饶。
    謝謝閱讀哑诊。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市及刻,隨后出現(xiàn)的幾起案子镀裤,更是在濱河造成了極大的恐慌,老刑警劉巖缴饭,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暑劝,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡颗搂,警方通過查閱死者的電腦和手機(jī)担猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丢氢,“玉大人傅联,你說我怎么就攤上這事【尾欤” “怎么了蒸走?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)貌嫡。 經(jīng)常有香客問我比驻,道長(zhǎng),這世上最難降的妖魔是什么岛抄? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任别惦,我火速辦了婚禮,結(jié)果婚禮上夫椭,老公的妹妹穿的比我還像新娘掸掸。我一直安慰自己,他們只是感情好蹭秋,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布扰付。 她就那樣靜靜地躺著,像睡著了一般感凤。 火紅的嫁衣襯著肌膚如雪悯周。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天陪竿,我揣著相機(jī)與錄音禽翼,去河邊找鬼。 笑死族跛,一個(gè)胖子當(dāng)著我的面吹牛闰挡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播礁哄,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼长酗,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了桐绒?” 一聲冷哼從身側(cè)響起夺脾,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤之拨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后咧叭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚀乔,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年菲茬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吉挣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡婉弹,死狀恐怖睬魂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情镀赌,我是刑警寧澤氯哮,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站佩脊,受9級(jí)特大地震影響蛙粘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜威彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一出牧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歇盼,春花似錦舔痕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至邢笙,卻和暖如春啸如,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背氮惯。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工叮雳, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妇汗。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓帘不,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親杨箭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子寞焙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容