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ù)緩沖起來等待渲染。 - 在
KFAudioDecoder的sampleBufferOutputCallBack回調(diào)中實現(xiàn)。
- 在
- 5)在渲染模塊
KFAudioRender的輸入數(shù)據(jù)回調(diào)中把緩沖區(qū)的數(shù)據(jù)交給系統(tǒng)音頻渲染單元渲染。 - 在
KFAudioRender的audioBufferInputCallBack回調(diào)中實現(xiàn)。
- 在
更具體細節(jié)見上述代碼及其注釋。
- 完 -
推薦閱讀