AVFoundation-绘制音频波形图AVAssetReader

AVAssetReader 和 AVAssetWriter类提供的低级功能,能处理更复杂的媒体样本。

AVAssetReader 和 AVAssetWriter

AVAssetReader

AVAssetReader 用于从 AVAsset示例中读取媒体样本。

通常会配置一个或多个AVAssetReaderOutput实例,并通过 copyNextSampleBuffer 方法访问音频和视频帧。

一个资源读取器的内部通道都是以多线程的方式不断提取下一个可用样本的。这样可以在系统请求资源时最小化时延。

AVAssetWriter

AVAssetWriter 用于对媒体资源进行编码并将其写入到容器文件中(如MPEG-4)。

通常由一个或多个AVAssetWriterInput对象配置,用于附加将包含要写入容器的媒体样本的CMSampleBuffer对象。

AVAssetWriter可用于实时操作和离线操作两种:

  • 实时。当处理实时资源时,比如从AVCaptureVideoDataOutput写入捕捉样本时,AVAssetWriterInput应该令expectsMediaDataInRealTime属性为YES类确保readyForMoreMediaData(指示保持数据样本交错的情况下是否可以附加更多信息)值被正确计算。
  • 离线。当从离线资源中读取媒体资源时,比如从AVAssetReader读取样本buffer,仍然需要readyForMoreMediaData,再可以使用requestMediaDataWhenReadyOnQueue:usingBlock方法来控制数据的提供。

创建波形图

创建波形需要
读取:读取音频样本进行渲染,需要读取或者解压音频数据。
缩减:读取到的样本远比我们需要的多,可以将样本分成小的样本块,并在每个样本块上找到最大值最小值和平均值。
渲染:将缩减后的样本呈现在屏幕上。

读取

使用AVAssetReader实例从AVAsset中读取音频样本并返回一个NSData对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//载入资源,读取asset样本
+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
completionBlock:(THSampleDataCompletionBlock)completionBlock {

NSString *tracks = @"tracks";
//异步载入键对应的资源
[asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{ // 1
//获取tracks键载入状态
AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];

NSData *sampleData = nil;
//如果载入成功,则从资源音频轨道中读取样本
if (status == AVKeyValueStatusLoaded) { // 2
sampleData = [self readAudioSamplesFromAsset:asset];
}

dispatch_async(dispatch_get_main_queue(), ^{ // 3
completionBlock(sampleData);
});
}];

}
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
+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {

NSError *error = nil;
//创建AVAssetReader实例
AVAssetReader *assetReader = // 1
[[AVAssetReader alloc] initWithAsset:asset error:&error];

if (!assetReader) {
NSLog(@"Error creating asset reader: %@", [error localizedDescription]);
return nil;
}
//获取资源中第一个音频轨道(最好是根据需求的媒体类型来获取轨道)
AVAssetTrack *track = // 2
[[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
//设置从资源轨道读取音频样本的解压设置,可以在AVAudioSettings.h文件中找到更多的键值
NSDictionary *outputSettings = @{ // 3
AVFormatIDKey : @(kAudioFormatLinearPCM),
AVLinearPCMIsBigEndianKey : @NO,
AVLinearPCMIsFloatKey : @NO,
AVLinearPCMBitDepthKey : @(16)
};

//创建AVAssetReaderTrackOutput 作为AVAssetReader的输出,并调用startReading来允许资源管理器开启预收取样本数据
AVAssetReaderTrackOutput *trackOutput = // 4
[[AVAssetReaderTrackOutput alloc] initWithTrack:track
outputSettings:outputSettings];

[assetReader addOutput:trackOutput];

[assetReader startReading];

NSMutableData *sampleData = [NSMutableData data];

while (assetReader.status == AVAssetReaderStatusReading) {
//调用跟踪输出的copyNextSampleBuffer开始每个迭代,每次都返回一个包含音频样本的下一个可用样本buffer
CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];// 5

if (sampleBuffer) {
//CMSampleBufferRef的音频样本被包含在CMBlockBufferRef类型中
CMBlockBufferRef blockBufferRef = // 6
CMSampleBufferGetDataBuffer(sampleBuffer);
//使用CMBlockBufferGetDataLength来确定blockBufferRef的长度,并使用一个16位带符号的整型数组来保存这写音频样本
size_t length = CMBlockBufferGetDataLength(blockBufferRef);
SInt16 sampleBytes[length];
//使用CMBlockBufferCopyDataBytes生成一个数组,内容来自CMBlockBufferRef包含的数据
CMBlockBufferCopyDataBytes(blockBufferRef, // 7
0,
length,
sampleBytes);
//将数组的内容附加到NSData实例后
[sampleData appendBytes:sampleBytes length:length];
//CMSampleBufferInvalidate函数来指定样本buffer已经处理和不可再继续使用,
CMSampleBufferInvalidate(sampleBuffer); // 8
//然后用CFRelease释放CMSampleBufferRef副本
CFRelease(sampleBuffer);
}
}
//如果资源读取器的状态为AVAssetReaderStatusCompleted表示数据被成功读取
if (assetReader.status == AVAssetReaderStatusCompleted) { // 9
return sampleData;
} else {
NSLog(@"Failed to read audio samples from asset");
return nil;
}
}

缩减音频样本

处理带有音频信息的NSData对象,根据指定的大小,将样本分成一个个样本块,找到样本块中的最大样本,得到筛选结果。

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
- (NSArray *)filteredSamplesForSize:(CGSize)size {

//filteredSamples保存筛选的样本数组
NSMutableArray *filteredSamples = [[NSMutableArray alloc] init]; // 1
//sampleCount表示样本总数
NSUInteger sampleCount = self.sampleData.length / sizeof(SInt16);
//binSize表示每个样本块的大小
NSUInteger binSize = sampleCount / size.width;

SInt16 *bytes = (SInt16 *) self.sampleData.bytes;

SInt16 maxSample = 0;
//迭代全部音频样本集合
for (NSUInteger i = 0; i < sampleCount; i += binSize) {
//样本块
SInt16 sampleBin[binSize];
//使用CFSwapInt16LittleToHost函数确保样本是按主机内置的字节顺序处理的
for (NSUInteger j = 0; j < binSize; j++) { // 2
sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
}
//找到样本块中的最大绝对值,并存入筛选结果
SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize]; // 3
[filteredSamples addObject:@(value)];

if (value > maxSample) { // 4
maxSample = value;
}
}
//约束筛选样本
CGFloat scaleFactor = (size.height / 2) / maxSample; // 5

for (NSUInteger i = 0; i < filteredSamples.count; i++) { // 6
filteredSamples[i] = @([filteredSamples[i] integerValue] * scaleFactor);
}

return filteredSamples;
}
1
2
3
4
5
6
7
8
9
10
//找到样本块中的最大绝对值
- (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {
SInt16 maxValue = 0;
for (int i = 0; i < size; i++) {
if (abs(values[i]) > maxValue) {
maxValue = abs(values[i]);
}
}
return maxValue;
}

渲染音频样本

创建UIView子类, 使用QuartzCore渲染筛选后的结果

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
- (void)drawRect:(CGRect)rect {

CGContextRef context = UIGraphicsGetCurrentContext();
//适当缩放上下文
CGContextScaleCTM(context, THWidthScaling, THHeightScaling); // 1
//适当在x y 轴上偏移
CGFloat xOffset = self.bounds.size.width -
(self.bounds.size.width * THWidthScaling);

CGFloat yOffset = self.bounds.size.height -
(self.bounds.size.height * THHeightScaling);

CGContextTranslateCTM(context, xOffset / 2, yOffset / 2);
//获取筛选后的音频样本
NSArray *filteredSamples = // 2
[self.filter filteredSamplesForSize:self.bounds.size];

CGFloat midY = CGRectGetMidY(rect);
//创建CGMutablePathRef对象 用来绘制Bezier路径的上半部
CGMutablePathRef halfPath = CGPathCreateMutable(); // 3
CGPathMoveToPoint(halfPath, NULL, 0.0f, midY);
//迭代样本 每次向路径中添加一个点
for (NSUInteger i = 0; i < filteredSamples.count; i++) {
float sample = [filteredSamples[i] floatValue];
CGPathAddLineToPoint(halfPath, NULL, i, midY - sample);
}

CGPathAddLineToPoint(halfPath, NULL, filteredSamples.count, midY);
//绘制完整波形
CGMutablePathRef fullPath = CGPathCreateMutable(); // 4
CGPathAddPath(fullPath, NULL, halfPath);
w
CGAffineTransform transform = CGAffineTransformIdentity; // 5
transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGPathAddPath(fullPath, &transform, halfPath);

CGContextAddPath(context, fullPath); // 6
CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
CGContextDrawPath(context, kCGPathFill);

CGPathRelease(halfPath); // 7
CGPathRelease(fullPath);
}