iOS音频数据读取--AVAssetReader和音频波形图绘制

本片分为数据的读取(使用到AVAssetReader),和重写drawRect方法将读取的数据绘制成图像。

一、AVAssetReader介绍

AVAssetReader用于从AVAsset实例中读取媒体样本。但光有它是不能解决我们的需求的,我们还得需要AVAssetReaderOutput输出实例并通过copyNextSampleBuffer方法将我们需要的数据给弄一份出来。注意读取的过程是一小部分一小部分的读取,并不是一次全部将所有数据速去完毕。我们创建一个NSData接受并将每次读取到的数据往后面附加,直到系统告诉我们所有数据已经读取完毕。AVAssetReader在执行读取操作的时候,它的status属性会伴随着读取进程的状态而发生改变,我们通过此属性来判断是否读取完毕。

- (NSData *)getRecorderDataFromURL:(NSURL *)url {
    
    NSMutableData *data = [[NSMutableData alloc]init];     //用于保存音频数据
    AVAsset *asset = [AVAsset assetWithURL:url];           //获取文件
  
    NSError *error;
    AVAssetReader *reader = [[AVAssetReader alloc]initWithAsset:asset error:&error]; //创建读取
    if (!reader) {
        
        NSLog(@"%@",[error localizedDescription]);
    }
    
    AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];//从媒体中得到声音轨道
    //读取配置
    NSDictionary *dic   = @{AVFormatIDKey            :@(kAudioFormatLinearPCM),
                            AVLinearPCMIsBigEndianKey:@NO,
                            AVLinearPCMIsFloatKey    :@NO,
                            AVLinearPCMBitDepthKey   :@(16)
                            };
    //读取输出,在相应的轨道和输出对应格式的数据
    AVAssetReaderTrackOutput *output = [[AVAssetReaderTrackOutput alloc]initWithTrack:track outputSettings:dic];
    //赋给读取并开启读取
    [reader addOutput:output];
    [reader startReading];
    
    //读取是一个持续的过程,每次只读取后面对应的大小的数据。当读取的状态发生改变时,其status属性会发生对应的改变,我们可以凭此判断是否完成文件读取
    while (reader.status == AVAssetReaderStatusReading) {
        
        CMSampleBufferRef  sampleBuffer = [output copyNextSampleBuffer]; //读取到数据
        if (sampleBuffer) {
            
            CMBlockBufferRef blockBUfferRef = CMSampleBufferGetDataBuffer(sampleBuffer);//取出数据
            size_t length = CMBlockBufferGetDataLength(blockBUfferRef);   //返回一个大小,size_t针对不同的品台有不同的实现,扩展性更好
            SInt16 sampleBytes[length];
            CMBlockBufferCopyDataBytes(blockBUfferRef, 0, length, sampleBytes); //将数据放入数组
            [data appendBytes:sampleBytes length:length];                 //将数据附加到data中
            CMSampleBufferInvalidate(sampleBuffer);  //销毁
            CFRelease(sampleBuffer);                 //释放
        }
    }
    if (reader.status == AVAssetReaderStatusCompleted) {
        
        self.audioData = data;

    }else{
        
        NSLog(@"获取音频数据失败");
        return nil;
    }
    
    //开始绘制波形图,重写了draw方法
    [self setNeedsDisplay];
    return data;

    
}

二、波形图的渲染

我们将数据读取完毕后,这些数据是十分多的,他们的个数单位用万来计数都不过分。我们采取的方式是抽样绘制。将所有数据分为一个个小包,每个小包中抽取一个最大值绘制。当然也可以算出每个小包的平均数,或者最小值。

绘制我们重写了drawRect:方法,在数据缩减完成将缩减的数据给它,使其生成对应的点绘制在屏幕上。在drawRect中调用了缩减数据的方法,缩减数据的方法中调用了获取数据的方法。这样就能在绘制类初始化的时候完成一切操作。注意的一点是,我们实现绘制的(展示绘制波形图的类)必须是继承UIView的。

//缩减音频
- (NSArray *)cutAudioData:(CGSize)size {
    
    NSMutableArray *filteredSamplesMA = [[NSMutableArray alloc]init];
    NSData *data = [self getRecorderDataFromURL:self.url];
    NSUInteger  sampleCount = data.length / sizeof(SInt16);           //计算所有数据个数
    NSUInteger  binSize     = sampleCount / size.width;               //将数据分割,也就是按照我们的需求width将数据分为一个个小包
    
    SInt16 *bytes = (SInt16 *)self.audioData.bytes;                   //总的数据个数
    SInt16 maxSample = 0;                                             //sint16两个字节的空间
    
    //以binSize为一个样本。每个样本中取一个最大数。也就是在固定范围取一个最大的数据保存,达到缩减目的
    for (NSUInteger i= 0; i < sampleCount; i += binSize) {//在sampleCount(所有数据)个数据中抽样,抽样方法为在binSize个数据为一个样本,在样本中选取一个数据
        
        SInt16 sampleBin [binSize];
        for (NSUInteger j = 0; j < binSize; j++) {//先将每次抽样样本的binSize个数据遍历出来
            
            sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
            
        }
        //选取样本数据中最大的一个数据
        SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize];
        //保存数据
        [filteredSamplesMA addObject:@(value)];
        //将所有数据中的最大数据保存,作为一个参考。可以根据情况对所有数据进行“缩放”
        if (value > maxSample) {
            
            maxSample = value;
        }
    }
    //计算比例因子
    CGFloat scaleFactor = (size.height/2)/maxSample;
    //对所有数据进行“缩放”
    for (NSUInteger i = 0; i < filteredSamplesMA.count; i++) {
        
        filteredSamplesMA[i] = @([filteredSamplesMA[i] integerValue] * scaleFactor);
    }
    
    NSLog(@"filteredSamplesMA====%ld",filteredSamplesMA.count);
    return filteredSamplesMA;
}

//比较大小的方法,返回最大值
- (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;
}

- (void)drawRect:(CGRect)rect  {
    
    CGContextRef context = UIGraphicsGetCurrentContext(); //当前上下文
    CGContextScaleCTM(context, 0.8, 0.8);                 //绘制区域相对于当前区域的比例,相当于缩放
    
    //缩放后将绘制图像移动
    CGFloat xOffset = self.bounds.size.width - (self.bounds.size.width*0.8);
    CGFloat yOffset = self.bounds.size.height - (self.bounds.size.height*0.8);
    CGContextTranslateCTM(context, xOffset/2, yOffset/2);
    
    NSArray *filerSamples = [self cutAudioData:self.bounds.size];      //得到绘制数据
    CGFloat midY = CGRectGetMidY(rect);                                //得到中心y的坐标
    CGMutablePathRef halfPath = CGPathCreateMutable();                 //绘制路径
    CGPathMoveToPoint(halfPath, nil, 0.0f, midY);      //在路径上移动当前画笔的位置到一个点,这个点由CGPoint 类型的参数指定。

    for (NSUInteger i = 0; i < filerSamples.count; i ++) {
        
        float sample = [filerSamples[i] floatValue];
        CGPathAddLineToPoint(halfPath, NULL, i, midY - sample);   //从当前的画笔位置向指定位置(同样由CGPoint类型的值指定)绘制线段
    }
    
    CGPathAddLineToPoint(halfPath, NULL, filerSamples.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, [UIColor cyanColor].CGColor);
    //开始绘制
    CGContextDrawPath(context, kCGPathFill);
    
    //移除
    CGPathRelease(halfPath);
    CGPathRelease(fullPath);
    [super drawRect:rect];

}

本文转自OSChina开原博客,感谢源作者:gitzhengjianhua

你可能感兴趣的:(iOS音频数据读取--AVAssetReader和音频波形图绘制)