iOS-柱状音频波形图绘制

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
23
24
25
26
//载入资源,读取asset样本

+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
completionBlock:(THSampleDataCompletionBlock)completionBlock {

NSString *tracks = @"tracks";

//异步载入键对应的资源

[asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{

//获取tracks键载入状态

AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
NSData *sampleData = nil;

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

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

缩减音频样本

处理带有音频信息的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
- (NSArray *)filteredSamplesForSize:(CGSize)size {
//filteredSamples保存筛选的样本数组
NSMutableArray *filteredSamples = [[NSMutableArray alloc] init];
//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++) {
sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
}
//找到样本块中的最大绝对值,并存入筛选结果
SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];
[filteredSamples addObject:@(value)];
if (value > maxSample) {
maxSample = value;
}
}

//约束筛选样本
CGFloat scaleFactor = (size.height / 2) / maxSample;

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

//找到样本块中的最大绝对值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (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
45
46
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//适当缩放上下文
CGContextScaleCTM(context, THWidthScaling, THHeightScaling);
//适当在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 = [self.filter filteredSamplesForSize:self.bounds.size];

CGFloat midY = CGRectGetMidY(rect);
//创建CGMutablePathRef对象 用来绘制Bezier路径的上半部
CGMutablePathRef halfPath = CGPathCreateMutable();
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();

CGPathAddPath(fullPath, NULL, halfPath);

CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));

transform = CGAffineTransformScale(transform, 1.0, -1.0);

CGPathAddPath(fullPath, &transform, halfPath);
CGContextAddPath(context, fullPath);

CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
CGContextDrawPath(context, kCGPathFill);
CGPathRelease(halfPath);
CGPathRelease(fullPath);

}
  Total:    No.