iOS AVDemo(6):音频渲染,代码开源并提供解析
介绍 iOS 音频渲染的流程和原理,并提供 Demo 源码和解析。
本文转自微信公众号
关键帧Keyframe
,推荐您关注来获取音视频、AI 领域的最新技术和产品信息:您还可以加入知识星球
关键帧的音视频开发圈
来一起交流工作中的技术难题、职场经验:
iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
过程,并借助音视频实用工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是第六篇:iOS 音频渲染 Demo。这个 Demo 里包含以下内容:
- 1)实现一个音频解封装模块;
- 2)实现一个音频解码模块;
- 3)实现一个音频渲染模块;
- 4)实现对 MP4 文件中音频部分的解封装和解码逻辑,并将解封装、解码后的数据送给渲染模块播放;
- 5)详尽的代码注释,帮你理解代码逻辑和原理。
你可以在关注本公众号后,在公众号发送消息『AVDemo』来获取 Demo 的全部源码。
1、音频解封装模块
在这个 Demo 中,解封装模块 KFMP4Demuxer
的实现与 《iOS 音频解封装 Demo》 中一样,这里就不再重复介绍了,其接口如下:
KFMP4Demuxer.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#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
回调中实现。
- 在
更具体细节见上述代码及其注释。