绘制一个音频波形基本包括以下三步:
代码地址为 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) }