转载

绘制音频波形图

绘制一个音频波形基本包括以下三步:

  1. 读取: 读取或解压音频样本
  2. 缩减: 实际读取的样本数量远比要渲染绘制的要多,缩减的过程必须作用于整个样本集.通常将样本总集分为固定大小的样本块,并在每个样本块上找到最大的样本、所有样本的平均值或min/max值.
  3. 渲染: 将缩减后的样本呈现在屏幕上

代码地址为 WaveformView ,编译环境为Xcode 7.3

读取音频样本

通过SampleDataProvider类实现读取音频样本的功能,读取的核心方法如下:

static func readAudioSamplesFromAVsset(asset:AVAsset) -> NSData? {
    //1. 创建一个AVAssetReader对象读取资源
    guard let assetReader = try? AVAssetReader(asset: asset) else{
        print("Unable to create AVAssetReader")
        return nil
    }
    //2. 获取资源中找到的第一个音频轨道
    guard let track = asset.tracksWithMediaType(AVMediaTypeAudio).first else{
        print("No audio track found in asset")
        return nil
    }
    //3. 从资源轨道读取音频样本时使用的解压设置
    //样本需要以未被压缩的格式读取(kAudioFormatLinearPCM)
    //样本以16位的little-endian字节顺序的有符号整型方式读取
    let outputSetting:[String:AnyObject] = [AVFormatIDKey:Int(kAudioFormatLinearPCM),
                         AVLinearPCMIsBigEndianKey:false,
                         AVLinearPCMIsFloatKey:false,
                         AVLinearPCMBitDepthKey:16
                         ]
    //4. 创建AVAssetReaderTrackOutput对象作为assetReader的输出
    let trackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: outputSetting)
    assetReader.addOutput(trackOutput)
    //5. 允许预收取样本数据
    assetReader.startReading()

    let sampleData = NSMutableData()

    while assetReader.status == .Reading {
        //6. 迭代返回包含一个音频样本的CMSampleBuffer
        if let sampleBuffer = trackOutput.copyNextSampleBuffer() {
            //7. CMSampleBuffer的音频样本被包含在一个CMBlockBuffer类型中
            if let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) {
                //8. 获取blockBuffer数据长度
                let length = CMBlockBufferGetDataLength(blockBuffer)
                //9. 拼接sampleData
                let sampleBytes = UnsafeMutablePointer<Int16>.alloc(length)
                CMBlockBufferCopyDataBytes(blockBuffer, 0, length, sampleBytes)
                sampleData.appendBytes(sampleBytes, length: length)
            }
        }
    }
    //10. 读取成功,返回数据
    if assetReader.status == .Completed {
        return sampleData
    }
    return nil
}

缩减音频样本

通过SampleDataFilter类实现缩减音频样本的功能,缩减的核心方法如下:

//按照指定的尺寸约束来筛选数据
func filteredSamplesForSize(size:CGSize) -> [Float] {
    /* 最终需要展示的样本集 */
    var filteredSamples = [Float]()
    //1. 每个样本为16字节,得到样本数量
    let samplesCount = self.data.length/sizeof(Int16.self)
    //2. 某个宽度范围内显示多少个样本数量
    let binSize = Int(samplesCount / Int(size.width))
    //3. 得到所有字节数据
    /* 注意创建数组作为buffer时,要先分配好内存,即需要指定数组长度 */
    var bytes = [Int16](count:self.data.length,repeatedValue:0)
    self.data.getBytes(&bytes, length: self.data.length)
    //4. 以binSize为步长遍历所有样本,
    var maxSample: Int16 = 0
    for i in 0.stride(to: samplesCount-1, by: binSize) {

        var sampleBin = [Int16](count:binSize,repeatedValue:0)
        for j in 0..<binSize {
            /*小端存储,低字节序*/
            sampleBin[j] = bytes[i + j].littleEndian
        }
        //5. 获取每个尺寸单位样本集binSize中的最大样本
        let value = self.maxValue(in: sampleBin, ofSize: binSize)
        //6. 添加到需要最终需要绘制展示的样本中
        filteredSamples.append(Float(value))
        if value > maxSample {
            maxSample = value
        }
    }
    //7 .根据所有样本中的最大样本值进行缩放
    let scaleFactor = (size.height / 2.0) / CGFloat(maxSample)
    //8. 对需要展示的样本进行缩放
    for i in 0..<filteredSamples.count {
        filteredSamples[i] = filteredSamples[i] * Float(scaleFactor)
    }
    return filteredSamples
}

渲染音频样本

创建UIView的子类WaveformView来渲染缩减结果,绘制的核心代码如下:

override func drawRect(rect: CGRect) {

    //1. 获取绘图上下文
    guard let context = UIGraphicsGetCurrentContext() else { return }
    //2. 获取需要进行绘制的数据
    guard let filteredSamples = filter?.filteredSamplesForSize(bounds.size) else {
        return
    }
    //3. 设置画布的缩放和上下左右间距
    CGContextScaleCTM(context, widthScaling, heightScaling);
    let xOffset = bounds.size.width - (bounds.size.width * widthScaling)
    let yOffset = bounds.size.height - (bounds.size.height * heightScaling)
    CGContextTranslateCTM(context, xOffset / 2, yOffset / 2);

    //4. 绘制上半部分
    let midY = CGRectGetMidY(rect)
    let halfPath = CGPathCreateMutable()
    CGPathMoveToPoint(halfPath, nil, 0.0, midY);
    for i in 0..<filteredSamples.count {
        let sample = CGFloat(filteredSamples[i])
        CGPathAddLineToPoint(halfPath, nil, CGFloat(i), midY - sample);
    }
    CGPathAddLineToPoint(halfPath, nil, CGFloat(filteredSamples.count), midY);

    //5. 绘制下半部分,对上半部分进行translate和sacle变化,即翻转上半部分
    let fullPath = CGPathCreateMutable()
    CGPathAddPath(fullPath, nil, halfPath);
    var transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
    transform = CGAffineTransformScale(transform, 1.0, -1.0);
    CGPathAddPath(fullPath, &transform, halfPath);

    //6. 将完整路径添加到上下文
    CGContextAddPath(context, fullPath);                                    
    CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
    CGContextDrawPath(context, .Fill);

}

override func layoutSubviews() {
    let size = loadingView.frame.size
    let x = (bounds.width - size.width) / 2.0
    let y = (bounds.height - size.height) / 2.0
    loadingView.frame = CGRect(x: x, y: y, width: size.width, height: size.height)
}
原文  http://www.devzhang.cn/2016/08/10/绘制音频波形图/
正文到此结束
Loading...