25 KiB
iOS AVDemo(6):音频渲染
iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
过程,并借助音视频工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是第六篇:iOS 音频渲染 Demo。这个 Demo 里包含以下内容:
- 1)实现一个音频解封装模块;
- 2)实现一个音频解码模块;
- 3)实现一个音频渲染模块;
- 4)实现对 MP4 文件中音频部分的解封装和解码逻辑,并将解封装、解码后的数据送给渲染模块播放;
- 5)详尽的代码注释,帮你理解代码逻辑和原理。
1、音频解封装模块
在这个 Demo 中,解封装模块 KFMP4Demuxer
的实现与 《iOS 音频解封装 Demo》 中一样,这里就不再重复介绍了,其接口如下:
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; // 是否包含音频数据。
@property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。
@property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。
@property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。
@property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。
@property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。
@property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。
@property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。
@property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。
- (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。
- (void)cancelReading; // 取消读取。
- (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。
- (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。
- (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。
- (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。
@end
NS_ASSUME_NONNULL_END
2、音频解码模块
同样的,解封装模块 KFAudioDecoder
的实现与 《iOS 音频解码 Demo》 中一样,这里就不再重复介绍了,其接口如下:
KFAudioDecoder.h
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
NS_ASSUME_NONNULL_BEGIN
@interface KFAudioDecoder : NSObject
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 解码器数据回调。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 解码器错误回调。
- (void)decodeSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 解码。
@end
NS_ASSUME_NONNULL_END
3、音频渲染模块
接下来,我们来实现一个音频渲染模块 KFAudioRender
,在这里输入解码后的数据进行渲染播放。
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); // 音频渲染数据输入回调。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 音频渲染错误回调。
@property (nonatomic, assign, readonly) NSInteger audioChannels; // 声道数。
@property (nonatomic, assign, readonly) NSInteger bitDepth; // 采样位深。
@property (nonatomic, assign, readonly) NSInteger audioSampleRate; // 采样率。
- (void)startPlaying; // 开始渲染。
- (void)stopPlaying; // 结束渲染。
@end
NS_ASSUME_NONNULL_END
上面是 KFAudioRender
接口的设计,除了初始化
接口,主要是有音频渲染数据输入回调
和错误回调
的接口,另外就是获取声道数
和获取采样率
的接口,以及开始渲染
和结束渲染
的接口。
这里重点需要看一下音频渲染数据输入回调
接口,系统的音频渲染单元每次会主动通过回调的方式要数据,我们这里封装的 KFAudioRender
则是用数据输入回调
接口来从外部获取一组待渲染的音频数据送给系统的音频渲染单元。
KFAudioRender.m
#import "KFAudioRender.h"
#define OutputBus 0
@interface KFAudioRender ()
@property (nonatomic, assign) AudioComponentInstance audioRenderInstance; // 音频渲染实例。
@property (nonatomic, assign, readwrite) NSInteger audioChannels; // 声道数。
@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 时创建音频渲染实例。
[weakSelf _setupAudioRenderInstance:&error];
if (error) {
// 捕捉并回调创建音频渲染实例时的错误。
[weakSelf _callBackError:error];
return;
}
}
// 开始渲染。
OSStatus status = AudioOutputUnitStart(weakSelf.audioRenderInstance);
if (status != noErr) {
// 捕捉并回调开始渲染时的错误。
[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);
// 捕捉并回调停止渲染时的错误。
if (status != noErr) {
[weakSelf _callBackError:[NSError errorWithDomain:NSStringFromClass([KFAudioRender class]) code:status userInfo:nil]];
}
}
});
}
#pragma mark - Private Method
- (void)_setupAudioRenderInstance:(NSError**)error {
// 1、设置音频组件描述。
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、创建音频组件实例。
OSStatus status = AudioComponentInstanceNew(inputComponent, &_audioRenderInstance);
if (status != noErr) {
*error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
return;
}
// 4、设置实例的属性:可读写。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、设置实例的属性:音频参数,如:数据格式、声道数、采样位深、采样率等。
AudioStreamBasicDescription inputFormat = {0};
inputFormat.mFormatID = kAudioFormatLinearPCM; // 原始数据为 PCM,采用声道交错格式。
inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
inputFormat.mChannelsPerFrame = (UInt32) self.audioChannels; // 每帧的声道数。
inputFormat.mFramesPerPacket = 1; // 每个数据包帧数。
inputFormat.mBitsPerChannel = (UInt32) self.bitDepth; // 采样位深。
inputFormat.mBytesPerFrame = inputFormat.mChannelsPerFrame * inputFormat.mBitsPerChannel / 8; // 每帧字节数 (byte = bit / 8)。
inputFormat.mBytesPerPacket = inputFormat.mFramesPerPacket * inputFormat.mBytesPerFrame; // 每个包字节数。
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、设置实例的属性:数据回调函数。
AURenderCallbackStruct renderCallbackRef = {
.inputProc = audioRenderCallback,
.inputProcRefCon = (__bridge void *) (self) // 对应回调函数中的 *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) {
// 通过音频渲染数据输入回调从外部获取待渲染的数据。
KFAudioRender *audioRender = (__bridge KFAudioRender *) inRefCon;
if (audioRender.audioBufferInputCallBack) {
audioRender.audioBufferInputCallBack(ioData);
}
return noErr;
}
@end
上面是 KFAudioRender
的实现,从代码上可以看到主要有这几个部分:
-
1)创建音频渲染实例。第一次调用
-startPlaying
才会创建音频渲染实例。 -
- 在
-_setupAudioRenderInstance:
方法中实现。
- 在
-
2)处理音频渲染实例的数据回调,并在回调中通过 KFAudioRender 的对外数据输入回调接口向更外层要待渲染的数据。
-
- 在
audioRenderCallback(...)
方法中实现回调处理逻辑。通过audioBufferInputCallBack
回调接口向更外层要数据。
- 在
-
3)实现开始渲染和停止渲染逻辑。
-
- 分别在
-startPlaying
和-stopPlaying
方法中实现。注意,这里是开始和停止操作都是放在串行队列中通过dispatch_async
异步处理的,这里主要是为了防止主线程卡顿。
- 分别在
-
4)捕捉音频渲染开始和停止操作中的错误,抛给 KFAudioRender 的对外错误回调接口。
-
- 在
-startPlaying
和-stopPlaying
方法中捕捉错误,在-_callBackError:
方法向外回调。
- 在
-
5)清理音频渲染实例。
-
- 在
-dealloc
方法中实现。
- 在
更具体细节见上述代码及其注释。
4、解封装和解码 MP4 文件中的音频部分并渲染播放
我们在一个 ViewController 中来实现从 MP4 文件中解封装和解码音频数据进行渲染播放。
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 // 解码数据缓冲区最大长度。
@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; // 解码数据缓冲区。
@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);
};
// 解码数据回调。在这里把解码后的音频 PCM 数据缓冲起来等待渲染。
_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;
// 这里设置的音频声道数、采样位深、采样率需要跟输入源的音频参数一致。
_audioRender = [[KFAudioRender alloc] initWithChannels:1 bitDepth:16 sampleRate:44100];
_audioRender.errorCallBack = ^(NSError* error) {
NSLog(@"KFAudioRender error:%zi %@", error.code, error.localizedDescription);
};
// 渲染输入数据回调。在这里把缓冲区的数据交给系统音频渲染单元渲染。
_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 来保证持续从文件中解封装和解码一定量的数据。
_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、设置分类。
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)的数据。
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 编码裸数据。
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t totolLength;
char *dataPointer = NULL;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totolLength, &dataPointer);
if (totolLength == 0 || !dataPointer) {
return;
}
// 目前 AudioDecoder 的解码接口实现的是单包(packet,1 packet 有 1024 帧)解码。而从 Demuxer 获取的一个 CMSampleBuffer 可能包含多个包,所以这里要拆一下包,再送给解码器。
NSLog(@"SampleNum: %ld", CMSampleBufferGetNumSamples(sampleBuffer));
for (NSInteger index = 0; index < CMSampleBufferGetNumSamples(sampleBuffer); index++) {
// 1、获取一个包的数据。
size_t sampleSize = CMSampleBufferGetSampleSize(sampleBuffer, index);
CMSampleTimingInfo timingInfo;
CMSampleBufferGetSampleTimingInfo(sampleBuffer, index, &timingInfo);
char *sampleDataPointer = malloc(sampleSize);
memcpy(sampleDataPointer, dataPointer, sampleSize);
// 2、将数据封装到 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、解码这个包的数据。
if (packetSampleBuffer) {
[self.decoder decodeSampleBuffer:packetSampleBuffer];
CFRelease(packetSampleBuffer);
}
}
dataPointer += sampleSize;
}
}
@end
上面是 KFAudioRenderViewController
的实现,其中主要包含这几个部分:
-
1)在页面加载完成后就启动解封装和解码模块,并通过一个 timer 来驱动解封装器和解码器。
-
- 在
-viewDidLoad
中实现。
- 在
-
2)定时从文件中解封装一定量(不超过 KFDecoderMaxCache)的数据送给解码器。
-
- 在
-timerCallBack:
方法中实现。
- 在
-
3)解封装后,需要将数据拆包,以包为单位封装为
CMSampleBuffer
送给解码器解码。 -
- 在
-decodeSampleBuffer:
方法中实现。
- 在
-
4)在解码模块
KFAudioDecoder
的数据回调中获取解码后的 PCM 数据缓冲起来等待渲染。 -
- 在
KFAudioDecoder
的sampleBufferOutputCallBack
回调中实现。
- 在
-
5)在渲染模块
KFAudioRender
的输入数据回调中把缓冲区的数据交给系统音频渲染单元渲染。 -
- 在
KFAudioRender
的audioBufferInputCallBack
回调中实现。
- 在
更具体细节见上述代码及其注释。