绘制一个音频波形基本包括以下三步:
- 读取: 读取或解压音频样本
- 缩减: 实际读取的样本数量远比要渲染绘制的要多,缩减的过程必须作用于整个样本集.通常将样本总集分为固定大小的样本块,并在每个样本块上找到最大的样本、所有样本的平均值或min/max值.
- 渲染: 将缩减后的样本呈现在屏幕上
代码地址为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)
}