基于iOS平臺的最簡單的FFmpeg音頻播放器(三)

  • 在前面兩個篇章中,已經把解碼和播放兩個重要的模塊講完了,剩下的只有一些處理中間邏輯的代碼了,看來這一篇是又要水了,但是也是重要要的處理部分。
  • 音頻解碼邏輯和之前的視頻解碼邏輯一樣樣的,下面我們可能只會列舉出一些不相同的部分。

基于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會很大,下載的時候比較費時。
  • 謝謝閱讀
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容