iOS AVDemo(6):音頻渲染,免費獲得源碼丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章。

畢加索《桌子》像素版

這個公眾號會路線圖 式的遍歷分享音視頻技術(shù)音視頻基礎(chǔ)(完成)音視頻工具(完成)音視頻工程示例(進行中) → 音視頻工業(yè)實戰(zhàn)(準備)。

iOS/Android 客戶端開發(fā)同學如果想要開始學習音視頻開發(fā),最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后,再借助 iOS/Android 平臺的音視頻能力上手去實踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)。

音視頻工程示例這個欄目,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)。

這里是第六篇:iOS 音頻渲染 Demo。這個 Demo 里包含以下內(nèi)容:

  • 1)實現(xiàn)一個音頻解封裝模塊;
  • 2)實現(xiàn)一個音頻解碼模塊;
  • 3)實現(xiàn)一個音頻渲染模塊;
  • 4)實現(xiàn)對 MP4 文件中音頻部分的解封裝和解碼邏輯,并將解封裝、解碼后的數(shù)據(jù)送給渲染模塊播放;
  • 5)詳盡的代碼注釋,幫你理解代碼邏輯和原理。

你可以在關(guān)注本公眾號后,在公眾號發(fā)送消息『AVDemo』來獲取 Demo 的全部源碼。

1、音頻解封裝模塊

在這個 Demo 中,解封裝模塊 KFMP4Demuxer 的實現(xiàn)與 《iOS 音頻解封裝 Demo》 中一樣,這里就不再重復(fù)介紹了,其接口如下:

KFMP4Demuxer.h

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFDemuxerConfig.h"

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) {
    KFMP4DemuxerStatusUnknown = 0,
    KFMP4DemuxerStatusRunning = 1,
    KFMP4DemuxerStatusFailed = 2,
    KFMP4DemuxerStatusCompleted = 3,
    KFMP4DemuxerStatusCancelled = 4,
};

@interface KFMP4Demuxer : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFDemuxerConfig *)config;

@property (nonatomic, strong, readonly) KFDemuxerConfig *config;
@property (nonatomic, copy) void (^errorCallBack)(NSError *error);
@property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音頻數(shù)據(jù)。
@property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含視頻數(shù)據(jù)。
@property (nonatomic, assign, readonly) CGSize videoSize; // 視頻大小。
@property (nonatomic, assign, readonly) CMTime duration; // 媒體時長。
@property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 編碼類型。
@property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封裝器狀態(tài)。
@property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音頻結(jié)束。
@property (nonatomic, assign, readonly) BOOL videoEOF; // 是否視頻結(jié)束。
@property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 圖像的變換信息。比如:視頻圖像旋轉(zhuǎn)。

- (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 開始讀取數(shù)據(jù)解封裝。
- (void)cancelReading; // 取消讀取。

- (BOOL)hasAudioSampleBuffer; // 是否還有音頻數(shù)據(jù)。
- (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷貝下一份音頻采樣。

- (BOOL)hasVideoSampleBuffer; // 是否還有視頻數(shù)據(jù)。
- (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷貝下一份視頻采樣。
@end

NS_ASSUME_NONNULL_END

2、音頻解碼模塊

同樣的,解封裝模塊 KFAudioDecoder 的實現(xiàn)與 《iOS 音頻解碼 Demo》 中一樣,這里就不再重復(fù)介紹了,其接口如下:

KFAudioDecoder.h

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

NS_ASSUME_NONNULL_BEGIN

@interface KFAudioDecoder : NSObject
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 解碼器數(shù)據(jù)回調(diào)。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 解碼器錯誤回調(diào)。

- (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 解碼。
@end

NS_ASSUME_NONNULL_END

3、音頻渲染模塊

接下來,我們來實現(xiàn)一個音頻渲染模塊 KFAudioRender,在這里輸入解碼后的數(shù)據(jù)進行渲染播放。

KFAudioRender.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

@class KFAudioRender;

NS_ASSUME_NONNULL_BEGIN

@interface KFAudioRender : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate;

@property (nonatomic, copy) void (^audioBufferInputCallBack)(AudioBufferList *audioBufferList); // 音頻渲染數(shù)據(jù)輸入回調(diào)。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音頻渲染錯誤回調(diào)。
@property (nonatomic, assign, readonly) NSInteger audioChannels; // 聲道數(shù)。
@property (nonatomic, assign, readonly) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readonly) NSInteger audioSampleRate; // 采樣率。

- (void)startPlaying; // 開始渲染。
- (void)stopPlaying; // 結(jié)束渲染。
@end

NS_ASSUME_NONNULL_END

上面是 KFAudioRender 接口的設(shè)計,除了初始化接口,主要是有音頻渲染數(shù)據(jù)輸入回調(diào)錯誤回調(diào)的接口,另外就是獲取聲道數(shù)獲取采樣率的接口,以及開始渲染結(jié)束渲染的接口。

這里重點需要看一下音頻渲染數(shù)據(jù)輸入回調(diào)接口,系統(tǒng)的音頻渲染單元每次會主動通過回調(diào)的方式要數(shù)據(jù),我們這里封裝的 KFAudioRender 則是用數(shù)據(jù)輸入回調(diào)接口來從外部獲取一組待渲染的音頻數(shù)據(jù)送給系統(tǒng)的音頻渲染單元。

KFAudioRender.m

#import "KFAudioRender.h"

#define OutputBus 0

@interface KFAudioRender ()
@property (nonatomic, assign) AudioComponentInstance audioRenderInstance; // 音頻渲染實例。
@property (nonatomic, assign, readwrite) NSInteger audioChannels; // 聲道數(shù)。
@property (nonatomic, assign, readwrite) NSInteger bitDepth; // 采樣位深。
@property (nonatomic, assign, readwrite) NSInteger audioSampleRate; // 采樣率。
@property (nonatomic, strong) dispatch_queue_t renderQueue;
@property (nonatomic, assign) BOOL isError;
@end

@implementation KFAudioRender
#pragma mark - Lifecycle
- (instancetype)initWithChannels:(NSInteger)channels bitDepth:(NSInteger)bitDepth sampleRate:(NSInteger)sampleRate {
    self = [super init];
    if (self) {
        _audioChannels = channels;
        _bitDepth = bitDepth;
        _audioSampleRate = sampleRate;
        _renderQueue = dispatch_queue_create("com.KeyFrameKit.audioRender", DISPATCH_QUEUE_SERIAL);
    }
    
    return self;
}

- (void)dealloc {
    // 清理音頻渲染實例。
    if (_audioRenderInstance) {
        AudioOutputUnitStop(_audioRenderInstance);
        AudioUnitUninitialize(_audioRenderInstance);
        AudioComponentInstanceDispose(_audioRenderInstance);
        _audioRenderInstance = nil;
    }
}

#pragma mark - Action
- (void)startPlaying {
    __weak typeof(self) weakSelf = self;
    dispatch_async(_renderQueue, ^{
        if (!weakSelf.audioRenderInstance) {
            NSError *error = nil;
            // 第一次 startPlaying 時創(chuàng)建音頻渲染實例。
            [weakSelf _setupAudioRenderInstance:&error];
            if (error) {
                // 捕捉并回調(diào)創(chuàng)建音頻渲染實例時的錯誤。
                [weakSelf _callBackError:error];
                return;
            }
        }
        
        // 開始渲染。
        OSStatus status = AudioOutputUnitStart(weakSelf.audioRenderInstance);
        if (status != noErr) {
            // 捕捉并回調(diào)開始渲染時的錯誤。
            [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
        }
    });
}

- (void)stopPlaying {
    __weak typeof(self) weakSelf = self;
    dispatch_async(_renderQueue, ^{
        if (weakSelf.audioRenderInstance && !self.isError) {
            // 停止渲染。
            OSStatus status = AudioOutputUnitStop(weakSelf.audioRenderInstance);
            // 捕捉并回調(diào)停止渲染時的錯誤。
            if (status != noErr) {
                [weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
            }
        }
    });
}

#pragma mark - Private Method
- (void)_setupAudioRenderInstance:(NSError**)error {
    // 1、設(shè)置音頻組件描述。
    AudioComponentDescription audioComponentDescription = {
        .componentType = kAudioUnitType_Output,
        //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回聲消除模式
        .componentSubType = kAudioUnitSubType_RemoteIO,
        .componentManufacturer = kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0
    };
    
    // 2、查找符合指定描述的音頻組件。
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioComponentDescription);

    // 3、創(chuàng)建音頻組件實例。
    OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioRenderInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 4、設(shè)置實例的屬性:可讀寫。0 不可讀寫,1 可讀寫。
    UInt32 flag = 1;
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OutputBus, &flag, sizeof(flag));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 5、設(shè)置實例的屬性:音頻參數(shù),如:數(shù)據(jù)格式、聲道數(shù)、采樣位深、采樣率等。
    AudioStreamBasicDescription inputFormat = {0};
    inputFormat.mFormatID = kAudioFormatLinearPCM; // 原始數(shù)據(jù)為 PCM,采用聲道交錯格式。
    inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    inputFormat.mChannelsPerFrame = (UInt32) self.audioChannels; // 每幀的聲道數(shù)。
    inputFormat.mFramesPerPacket = 1; // 每個數(shù)據(jù)包幀數(shù)。
    inputFormat.mBitsPerChannel = (UInt32) self.bitDepth; // 采樣位深。
    inputFormat.mBytesPerFrame = inputFormat.mChannelsPerFrame * inputFormat.mBitsPerChannel / 8; // 每幀字節(jié)數(shù) (byte = bit / 8)。
    inputFormat.mBytesPerPacket = inputFormat.mFramesPerPacket * inputFormat.mBytesPerFrame; // 每個包字節(jié)數(shù)。
    inputFormat.mSampleRate = self.audioSampleRate; // 采樣率
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, OutputBus, &inputFormat, sizeof(inputFormat));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }

    // 6、設(shè)置實例的屬性:數(shù)據(jù)回調(diào)函數(shù)。
    AURenderCallbackStruct renderCallbackRef = {
        .inputProc = audioRenderCallback,
        .inputProcRefCon = (__bridge void *) (self) // 對應(yīng)回調(diào)函數(shù)中的 *inRefCon。
    };
    status = AudioUnitSetProperty(_audioRenderInstance, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, OutputBus, &renderCallbackRef, sizeof(renderCallbackRef));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 7、初始化實例。
    status = AudioUnitInitialize(_audioRenderInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
}

- (void)_callBackError:(NSError*)error {
    self.isError = YES;
    if (self.errorCallBack) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.errorCallBack(error);
        });
    }
}

#pragma mark - Render Callback
static OSStatus audioRenderCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inOutputBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) {
    // 通過音頻渲染數(shù)據(jù)輸入回調(diào)從外部獲取待渲染的數(shù)據(jù)。
    KFAudioRender *audioRender = (__bridge KFAudioRender *) inRefCon;
    if (audioRender.audioBufferInputCallBack) {
        audioRender.audioBufferInputCallBack(ioData);
    }
    
    return noErr;
}

@end

上面是 KFAudioRender 的實現(xiàn),從代碼上可以看到主要有這幾個部分:

  • 1)創(chuàng)建音頻渲染實例。第一次調(diào)用 -startPlaying 才會創(chuàng)建音頻渲染實例。
    • -_setupAudioRenderInstance: 方法中實現(xiàn)。
  • 2)處理音頻渲染實例的數(shù)據(jù)回調(diào),并在回調(diào)中通過 KFAudioRender 的對外數(shù)據(jù)輸入回調(diào)接口向更外層要待渲染的數(shù)據(jù)。
    • audioRenderCallback(...) 方法中實現(xiàn)回調(diào)處理邏輯。通過 audioBufferInputCallBack 回調(diào)接口向更外層要數(shù)據(jù)。
  • 3)實現(xiàn)開始渲染和停止渲染邏輯。
    • 分別在 -startPlaying-stopPlaying 方法中實現(xiàn)。注意,這里是開始和停止操作都是放在串行隊列中通過 dispatch_async 異步處理的,這里主要是為了防止主線程卡頓。
  • 4)捕捉音頻渲染開始和停止操作中的錯誤,拋給 KFAudioRender 的對外錯誤回調(diào)接口。
    • -startPlaying-stopPlaying 方法中捕捉錯誤,在 -_callBackError: 方法向外回調(diào)。
  • 5)清理音頻渲染實例。
    • -dealloc 方法中實現(xiàn)。

更具體細節(jié)見上述代碼及其注釋。

4、解封裝和解碼 MP4 文件中的音頻部分并渲染播放

我們在一個 ViewController 中來實現(xiàn)從 MP4 文件中解封裝和解碼音頻數(shù)據(jù)進行渲染播放。

KFAudioRenderViewController.m

#import "KFAudioRenderViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "KFAudioRender.h"
#import "KFMP4Demuxer.h"
#import "KFAudioDecoder.h"
#import "KFWeakProxy.h"

#define KFDecoderMaxCache 4096 * 5 // 解碼數(shù)據(jù)緩沖區(qū)最大長度。

@interface KFAudioRenderViewController ()
@property (nonatomic, strong) KFDemuxerConfig *demuxerConfig;
@property (nonatomic, strong) KFMP4Demuxer *demuxer;
@property (nonatomic, strong) KFAudioDecoder *decoder;
@property (nonatomic, strong) KFAudioRender *audioRender;
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, strong) NSMutableData *pcmDataCache; // 解碼數(shù)據(jù)緩沖區(qū)。
@property (nonatomic, assign) NSInteger pcmDataCacheLength;
@property (nonatomic, strong) CADisplayLink *timer;
@end

@implementation KFAudioRenderViewController
#pragma mark - Property
- (KFDemuxerConfig *)demuxerConfig {
    if (!_demuxerConfig) {
        _demuxerConfig = [[KFDemuxerConfig alloc] init];
        _demuxerConfig.demuxerType = KFMediaAudio;
        NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"input" ofType:@"mp4"];
        _demuxerConfig.asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
    }
    
    return _demuxerConfig;
}

- (KFMP4Demuxer *)demuxer {
    if (!_demuxer) {
        _demuxer = [[KFMP4Demuxer alloc] initWithConfig:self.demuxerConfig];
        _demuxer.errorCallBack = ^(NSError *error) {
            NSLog(@"KFMP4Demuxer error:%zi %@", error.code, error.localizedDescription);
        };
    }
    
    return _demuxer;
}

- (KFAudioDecoder *)decoder {
    if (!_decoder) {
        __weak typeof(self) weakSelf = self;
        _decoder = [[KFAudioDecoder alloc] init];
        _decoder.errorCallBack = ^(NSError *error) {
            NSLog(@"KFAudioDecoder error:%zi %@", error.code, error.localizedDescription);
        };
        // 解碼數(shù)據(jù)回調(diào)。在這里把解碼后的音頻 PCM 數(shù)據(jù)緩沖起來等待渲染。
        _decoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            if (sampleBuffer) {
                CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
                size_t totolLength;
                char *dataPointer = NULL;
                CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
                if (totolLength == 0 || !dataPointer) {
                    return;
                }
                dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
                [weakSelf.pcmDataCache appendData:[NSData dataWithBytes:dataPointer length:totolLength]];
                weakSelf.pcmDataCacheLength += totolLength;
                dispatch_semaphore_signal(weakSelf.semaphore);
            }
        };
    }
    
    return _decoder;
}

- (KFAudioRender *)audioRender {
    if (!_audioRender) {
        __weak typeof(self) weakSelf = self;
        // 這里設(shè)置的音頻聲道數(shù)、采樣位深、采樣率需要跟輸入源的音頻參數(shù)一致。
        _audioRender = [[KFAudioRender alloc] initWithChannels:1 bitDepth:16 sampleRate:44100];
        _audioRender.errorCallBack = ^(NSError* error) {
            NSLog(@"KFAudioRender error:%zi %@", error.code, error.localizedDescription);
        };
        // 渲染輸入數(shù)據(jù)回調(diào)。在這里把緩沖區(qū)的數(shù)據(jù)交給系統(tǒng)音頻渲染單元渲染。
        _audioRender.audioBufferInputCallBack = ^(AudioBufferList * _Nonnull audioBufferList) {
            if (weakSelf.pcmDataCacheLength < audioBufferList->mBuffers[0].mDataByteSize) {
                memset(audioBufferList->mBuffers[0].mData, 0, audioBufferList->mBuffers[0].mDataByteSize);
            } else {
                dispatch_semaphore_wait(weakSelf.semaphore, DISPATCH_TIME_FOREVER);
                memcpy(audioBufferList->mBuffers[0].mData, weakSelf.pcmDataCache.bytes, audioBufferList->mBuffers[0].mDataByteSize);
                [weakSelf.pcmDataCache replaceBytesInRange:NSMakeRange(0, audioBufferList->mBuffers[0].mDataByteSize) withBytes:NULL length:0];
                weakSelf.pcmDataCacheLength -= audioBufferList->mBuffers[0].mDataByteSize;
                dispatch_semaphore_signal(weakSelf.semaphore);
            }
        };
    }
    
    return _audioRender;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];
        
    _semaphore = dispatch_semaphore_create(1);
    _pcmDataCache = [[NSMutableData alloc] init];
    
    [self setupAudioSession];
    [self setupUI];
    
    // 通過一個 timer 來保證持續(xù)從文件中解封裝和解碼一定量的數(shù)據(jù)。
    _timer = [CADisplayLink displayLinkWithTarget:[KFWeakProxy proxyWithTarget:self] selector:@selector(timerCallBack:)];
    [_timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    [_timer setPaused:NO];
    
    [self.demuxer startReading:^(BOOL success, NSError * _Nonnull error) {
        NSLog(@"KFMP4Demuxer start:%d", success);
    }];
}

- (void)dealloc {
    
}

#pragma mark - Setup
- (void)setupUI {
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.title = @"Audio Render";
    self.view.backgroundColor = [UIColor whiteColor];
    
    
    // Navigation item.
    UIBarButtonItem *startRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(startRender)];
    UIBarButtonItem *stopRenderBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stopRender)];
    self.navigationItem.rightBarButtonItems = @[startRenderBarButton, stopRenderBarButton];
}

#pragma mark - Action
- (void)startRender {
    [self.audioRender startPlaying];
}

- (void)stopRender {
    [self.audioRender stopPlaying];
}

#pragma mark - Utility
- (void)setupAudioSession {
    // 1、獲取音頻會話實例。
    AVAudioSession *session = [AVAudioSession sharedInstance];
    
    // 2、設(shè)置分類。
    NSError *error = nil;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
    if (error) {
        NSLog(@"AVAudioSession setCategory error");
    }
    
    // 3、激活會話。
    [session setActive:YES error:&error];
    if (error) {
        NSLog(@"AVAudioSession setActive error");
    }
}

- (void)timerCallBack:(CADisplayLink *)link {
    // 定時從文件中解封裝和解碼一定量(不超過 KFDecoderMaxCache)的數(shù)據(jù)。
    if (self.pcmDataCacheLength <  KFDecoderMaxCache && self.demuxer.demuxerStatus == KFMP4DemuxerStatusRunning && self.demuxer.hasAudioSampleBuffer) {
        CMSampleBufferRef audioBuffer = [self.demuxer copyNextAudioSampleBuffer];
        if (audioBuffer) {
            [self decodeSampleBuffer:audioBuffer];
            CFRelease(audioBuffer);
        }
    }
}

- (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 獲取解封裝后的 AAC 編碼裸數(shù)據(jù)。
    CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t totolLength;
    char *dataPointer = NULL;
    CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
    if (totolLength == 0 || !dataPointer) {
        return;
    }
    
    // 目前 AudioDecoder 的解碼接口實現(xiàn)的是單包(packet,1 packet 有 1024 幀)解碼。而從 Demuxer 獲取的一個 CMSampleBuffer 可能包含多個包,所以這里要拆一下包,再送給解碼器。
    NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer));
    for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) {
        // 1、獲取一個包的數(shù)據(jù)。
        size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index);
        CMSampleTimingInfo timingInfo;
        CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo);
        char *sampleDataPointer = malloc(sampleSize);
        memcpy(sampleDataPointer, dataPointer, sampleSize);
        
        // 2、將數(shù)據(jù)封裝到 CMBlockBuffer 中。
        CMBlockBufferRef packetBlockBuffer;
        OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,
                                                              sampleDataPointer,
                                                              sampleSize,
                                                              NULL,
                                                              NULL,
                                                              0,
                                                              sampleSize,
                                                              0,
                                                              &packetBlockBuffer);
        
        if (status == noErr) {
            // 3、將 CMBlockBuffer 封裝到 CMSampleBuffer 中。
            CMSampleBufferRef packetSampleBuffer = NULL;
            const size_t sampleSizeArray[] = {sampleSize};
            status = CMSampleBufferCreateReady(kCFAllocatorDefault,
                                               packetBlockBuffer,
                                               CMSampleBufferGetFormatDescription(sampleBuffer),
                                               1,
                                               1,
                                               &timingInfo,
                                               1,
                                               sampleSizeArray,
                                               &packetSampleBuffer);
            CFRelease(packetBlockBuffer);
            
            // 4、解碼這個包的數(shù)據(jù)。
            if (packetSampleBuffer) {
                [self.decoder decodeSampleBuffer:packetSampleBuffer];
                CFRelease(packetSampleBuffer);
            }
        }
        dataPointer += sampleSize;
    }
}

@end

上面是 KFAudioRenderViewController 的實現(xiàn),其中主要包含這幾個部分:

  • 1)在頁面加載完成后就啟動解封裝和解碼模塊,并通過一個 timer 來驅(qū)動解封裝器和解碼器。
    • -viewDidLoad 中實現(xiàn)。
  • 2)定時從文件中解封裝一定量(不超過 KFDecoderMaxCache)的數(shù)據(jù)送給解碼器。
    • -timerCallBack: 方法中實現(xiàn)。
  • 3)解封裝后,需要將數(shù)據(jù)拆包,以包為單位封裝為 CMSampleBuffer 送給解碼器解碼。
    • -decodeSampleBuffer: 方法中實現(xiàn)。
  • 4)在解碼模塊 KFAudioDecoder 的數(shù)據(jù)回調(diào)中獲取解碼后的 PCM 數(shù)據(jù)緩沖起來等待渲染。
    • KFAudioDecodersampleBufferOutputCallBack 回調(diào)中實現(xiàn)。
  • 5)在渲染模塊 KFAudioRender 的輸入數(shù)據(jù)回調(diào)中把緩沖區(qū)的數(shù)據(jù)交給系統(tǒng)音頻渲染單元渲染。
    • KFAudioRenderaudioBufferInputCallBack 回調(diào)中實現(xiàn)。

更具體細節(jié)見上述代碼及其注釋。

- 完 -

推薦閱讀

《iOS AVDemo(5):音頻解碼》

《iOS AVDemo(4):音頻解封裝》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容