第二篇:一步一步教你实现iOS音频频谱动画(二)html
基于篇幅考虑,本次教程分为两篇文章,本篇文章主要讲述音频播放和频谱数据的获取,下篇将讲述数据处理和动画绘制。ios
好久之前在电脑上听音乐的时候,常常会调出播放器的一个小工具,里面的柱状图会随着音乐节奏而跳动,就感受本身好专业,尽管后来才知道这个是音频信号在频域下的表现。git
动手写代码以前,让咱们先了解几个基础概念吧github
采样: 众所周知,声音是一种压力波,是连续的,然而在计算机中没法表示连续的数据,因此只能经过间隔采样的方式进行离散化,其中采集的频率称为采样率。根据奈奎斯特采样定理 ,当采样率大于信号最高频率的2倍时信号频率不会失真。人类能听到的声音频率范围是20hz到20khz,因此CD等采用了44.1khz采样率能知足大部分须要。算法
量化: 每次采样的信号强度也会有精度的损失,若是用16位的Int(位深度)来表示,它的范围是[-32768,32767],所以位深度越高可表示的范围就越大,音频质量越好。编程
声道数: 为了更好的效果,声音通常采集左右双声道的信号,如何编排呢?一种是采用交错排列(Interleaved): LRLRLRLR
,另外一种采用各自排列(non-Interleaved): LLL RRR
。swift
以上将模拟信号数字化的方法称为脉冲编码调制(PCM),而本文中咱们就须要对这类数据进行加工处理。数组
如今咱们的音频数据是时域的,也就是说横轴是时间,纵轴是信号的强度,而动画展示要求的横轴是频率。将信号从时域转换成频域可使用傅里叶变换实现,信号通过傅里叶变换分解成了不一样频率的正弦波,这些信号的频率和振幅就是咱们须要实现动画的数据。数据结构
实际上计算机中处理的都是离散傅里叶变换(DFT),而快速傅里叶变换(FFT)是快速计算离散傅里叶变换(DFT)或其逆变换的方法,它将DFT的复杂度从O(n²)下降到O(nlogn)。 若是你刚才点开前面连接看过其中介绍的FFT算法,那么可能会以为这FFT代码该怎么写?不用担忧,苹果为此提供了Accelerate框架,其中vDSP部分提供了数字信号处理的函数实现,包含FFT。有了vDSP,咱们只需几个步骤便可实现FFT,简单便捷,并且性能高效。app
如今开始让咱们看一下iOS系统中的音频框架, AudioToolbox
功能强大,不过提供的API都是基于C语言的,其大多数功能已经能够经过AVFoundation
实现,它利用Objective-C
/Swift
对于底层接口进行了封装。咱们本次需求比较简单,只须要播放音频文件并进行实时处理,因此AVFoundation
中的AVAudioEngine
就能知足本次音频播放和处理的须要。
AVAudioEngine
从iOS8加入到AVFoundation
框架,它提供了之前须要深刻到底层AudioToolbox
才实现的功能,好比实时音频处理。它把音频处理的各环节抽象成AVAudioNode
并经过AVAudioEngine
进行管理,最后将它们链接构成完整的节点图。如下就是本次教程的AVAudioEngine
与其节点的链接方式。
图3 AVAudioEngine和AVAudioNode链接图
mainMixerNode
和outputNode
都是在被访问的时候默认由AVAudioEngine
对象建立并链接的单例对象,也就是说咱们只须要手动建立engine
和player
节点并将他们链接就能够了!最后在mainMixerNode
的输出总线上安装分接头将定量输出的AVAudioPCMBuffer
数据进行转换和FFT。
了解了以上相关知识后,咱们就能够开始编写代码了。打开项目AudioSpectrum01-starter
,首先要实现的是音频播放功能。
若是你只是想浏览实现代码,打开项目
AudioSpectrum01-final
便可,已经完成本篇文章的全部代码
在AudioSpectrumPlayer
类中建立AVAudioEngine
和AVAudioPlayerNode
两个实例变量:
private let engine = AVAudioEngine()
private let player = AVAudioPlayerNode()
复制代码
接下来在init()
方法中添加以下代码:
//1
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: nil)
//2
engine.prepare()
try! engine.start()
复制代码
//1
:这里将player
挂载到engine
上,再把player
与engine
的mainMixerNode
链接起来就完成了AVAudioEngine
的整个节点图建立(详见图3)。
//2
:在调用engine
的strat()
方法开始启动engine
以前,须要经过prepare()
方法提早分配相关资源
继续完善play(withFileName fileName: String)
和stop()
方法:
//1
func play(withFileName fileName: String) {
guard let audioFileURL = Bundle.main.url(forResource: fileName, withExtension: nil),
let audioFile = try? AVAudioFile(forReading: audioFileURL) else { return }
player.stop()
player.scheduleFile(audioFile, at: nil, completionHandler: nil)
player.play()
}
//2
func stop() {
player.stop()
}
复制代码
//1
:首先须要确保文件名为fileName
的音频文件能正常加载,而后经过stop()
方法中止以前的播放,再调用scheduleFile(_:at:completionHandler:)
方法编排新的文件,最后经过play()
方法开始播放。
//2
:中止播放调用player
的stop()
方法便可。
音频播放功能已经完成,运行项目,试试点击音乐右侧的Play
按钮进行音频播放吧。
前面提到咱们能够在mainMixerNode
上安装分接头定量获取AVAudioPCMBuffer
数据,如今打开AudioSpectrumPlayer
文件,先定义一个属性: fftSize
,它是每次获取到的buffer
的frame数量。
private var fftSize: Int = 2048
复制代码
将光标定位至init()
方法中的engine.connect()
语句下方,调用mainMixerNode
的installTap
方法开始安装分接头:
engine.mainMixerNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(fftSize), format: nil, block: { [weak self](buffer, when) in
guard let strongSelf = self else { return }
if !strongSelf.player.isPlaying { return }
buffer.frameLength = AVAudioFrameCount(strongSelf.fftSize) //这句的做用是确保每次回调中buffer的frameLength是fftSize大小,详见:https://stackoverflow.com/a/27343266/6192288
let amplitudes = strongSelf.fft(buffer)
if strongSelf.delegate != nil {
strongSelf.delegate?.player(strongSelf, didGenerateSpectrum: amplitudes)
}
})
复制代码
在分接头的回调 block 中将拿到的 2048 个 frame 的 buffer 交由fft
函数进行计算,最后将计算的结果经过delegate
进行传递。
按照 44100hz 采样率和 1 Frame = 1 Packet 来计算(能够参考这里关于channel、sample、frame、packet的概念与关系),那么block将会在一秒中调用44100/2048≈21.5次左右,另外须要注意的是block有可能不在主线程调用。
终于到实现FFT
的时候了,根据vDSP
文档,首先须要定义一个FFT
的权重数组(fftSetup)
,它能够在屡次FFT
中重复使用和提高FFT
性能:
private lazy var fftSetup = vDSP_create_fftsetup(vDSP_Length(Int(round(log2(Double(fftSize))))), FFTRadix(kFFTRadix2))
复制代码
不须要时在析构函数(反初始化函数)中销毁:
deinit {
vDSP_destroy_fftsetup(fftSetup)
}
复制代码
最后新建fft
函数,实现代码以下:
private func fft(_ buffer: AVAudioPCMBuffer) -> [[Float]] {
var amplitudes = [[Float]]()
guard let floatChannelData = buffer.floatChannelData else { return amplitudes }
//1:抽取buffer中的样本数据
var channels: UnsafePointer<UnsafeMutablePointer<Float>> = floatChannelData
let channelCount = Int(buffer.format.channelCount)
let isInterleaved = buffer.format.isInterleaved
if isInterleaved {
// deinterleave
let interleavedData = UnsafeBufferPointer(start: floatChannelData[0], count: self.fftSize * channelCount)
var channelsTemp : [UnsafeMutablePointer<Float>] = []
for i in 0..<channelCount {
var channelData = stride(from: i, to: interleavedData.count, by: channelCount).map{ interleavedData[$0]}
channelsTemp.append(UnsafeMutablePointer(&channelData))
}
channels = UnsafePointer(channelsTemp)
}
for i in 0..<channelCount {
let channel = channels[i]
//2: 加汉宁窗
var window = [Float](repeating: 0, count: Int(fftSize))
vDSP_hann_window(&window, vDSP_Length(fftSize), Int32(vDSP_HANN_NORM))
vDSP_vmul(channel, 1, window, 1, channel, 1, vDSP_Length(fftSize))
//3: 将实数包装成FFT要求的复数fftInOut,既是输入也是输出
var realp = [Float](repeating: 0.0, count: Int(fftSize / 2))
var imagp = [Float](repeating: 0.0, count: Int(fftSize / 2))
var fftInOut = DSPSplitComplex(realp: &realp, imagp: &imagp)
channel.withMemoryRebound(to: DSPComplex.self, capacity: fftSize) { (typeConvertedTransferBuffer) -> Void in
vDSP_ctoz(typeConvertedTransferBuffer, 2, &fftInOut, 1, vDSP_Length(fftSize / 2))
}
//4:执行FFT
vDSP_fft_zrip(fftSetup!, &fftInOut, 1, vDSP_Length(round(log2(Double(fftSize)))), FFTDirection(FFT_FORWARD));
//5:调整FFT结果,计算振幅
fftInOut.imagp[0] = 0
let fftNormFactor = Float(1.0 / (Float(fftSize)))
vDSP_vsmul(fftInOut.realp, 1, [fftNormFactor], fftInOut.realp, 1, vDSP_Length(fftSize / 2));
vDSP_vsmul(fftInOut.imagp, 1, [fftNormFactor], fftInOut.imagp, 1, vDSP_Length(fftSize / 2));
var channelAmplitudes = [Float](repeating: 0.0, count: Int(fftSize / 2))
vDSP_zvabs(&fftInOut, 1, &channelAmplitudes, 1, vDSP_Length(fftSize / 2));
channelAmplitudes[0] = channelAmplitudes[0] / 2 //直流份量的振幅须要再除以2
amplitudes.append(channelAmplitudes)
}
return amplitudes
}
复制代码
经过代码中的注释,应该能了解如何从buffer
获取音频样本数据并进行FFT
计算了,不过如下两点是我在完成这一部份内容过程当中比较难理解的部分:
buffer
对象的方法floatChannelData
获取样本数据,若是是多声道而且是interleaved
,咱们就须要对它进行deinterleave
, 经过下图就能比较清楚的知道deinterleave
先后的结构,不过在我试验了许多音频文件以后,发现都是non-interleaved
的,也就是无需进行转换。┑( ̄Д  ̄)┍vDSP
在进行实数FFT
计算时利用一种独特的数据格式化方式以达到节省内存的目的,它在FFT
计算的先后经过两次转换将FFT
的输入和输出的数据结构进行统一成DSPSplitComplex
。第一次转换是经过vDSP_ctoz
函数将样本数据的实数数组转换成DSPSplitComplex
。第二次则是将FFT
结果转换成DSPSplitComplex
,这个转换的过程是在FFT
计算函数vDSP_fft_zrip
中自动完成的。
第二次转换过程以下:n位样本数据(n/2位复数)进行fft计算会获得n/2+1位复数结果:{[DC,0],C[2],...,C[n/2],[NY,0]} (其中DC是直流份量,NY是奈奎斯特频率的值,C是复数数组),其中[DC,0]和[NY,0]的虚部都是0,因此能够将NY放到DC中的虚部中,其结果变成{[DC,NY],C[2],C[3],...,C[n/2]},与输入位数一致。
再次运行项目,如今除了听到音乐以外还能够在控制台中看到打印输出的频谱数据。
好了,本篇文章内容到这里就结束了,下一篇文章将对计算好的频谱数据进行处理和动画绘制。
资料参考
[1] wikipedia,脉冲编码调制, zh.wikipedia.org/wiki/%E8%84…
[2] Mike Ash,音频数据获取与解析, www.mikeash.com/pyblog/frid…
[3] 韩 昊, 傅里叶分析之掐死教程, blog.jobbole.com/70549/
[4] raywenderlich, AVAudioEngine编程入门,www.raywenderlich.com/5154-avaudi…
[5] Apple, vDSP编程指南, developer.apple.com/library/arc…
[6] Apple, aurioTouch案例代码,developer.apple.com/library/arc…