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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容