如果你想先看看最终效果再决定看不看文章 ->
示例代码下载
第一篇:一步一步教你实现iOS音频频谱动画(一)
本文是系列文章中的第二篇,上篇讲述了音频播放和频谱数据计算,本篇讲述数据处理和动画的绘制。
前言
在上篇文章中我们已经拿到了频谱数据,也知道了数组每个元素表示的是振幅,那这些数组元素之间有什么关系呢?根据FFT
的原理, N个音频信号样本参与计算将产生N/2个数据(2048/2=1024),其频率分辨率△f=Fs/N = 44100/2048≈21.5hz,而相邻数据的频率间隔是一样的,因此这1024个数据分别代表频率在0hz、21.5hz、43.0hz….22050hz下的振幅。
那是不是可以直接将这1024个数据绘制成动画?当然可以,如果你刚好要显示1024个动画物件!但是如果你想可以灵活地调整这个数量,那么需要进行频带划分。
严格来说,结果有1025个,因为在上篇文章的
FFT
计算中通过fftInOut.imagp[0] = 0
,直接把第1025个值舍弃掉了。这第1025个值代表的是奈奎斯特频率值的实部。至于为什么保存在第一个FFT
结果的虚部中,请翻看第一篇。
频带划分
频带划分更重要的原因其实是这样的:根据心理声学,人耳能容易的分辨出100hz和200hz的音调不同,但是很难分辨出8100hz和8200hz的音调不同,尽管它们各自都是相差100hz,可以说频率和音调之间的变化并不是呈线性关系,而是某种对数的关系。因此在实现动画时将数据从等频率间隔划分成对数增长的间隔更合乎人类的听感。
图1 频带划分方式
打开项目AudioSpectrum02-starter
,您会发现跟之前的AudioSpectrum01
项目有些许不同,它将FFT
相关的计算移到了新增的类RealtimeAnalyzer
中,使得AudioSpectrumPlayer
和RealtimeAnalyzer
两个类的职责更为明确。
如果你只是想浏览实现代码,打开项目
AudioSpectrum02-final
即可,已经完成本篇文章的所有代码
查看RealtimeAnalyzer
类的代码,其中已经定义了 frequencyBands、startFrequency、endFrequency 三个属性,它们将决定频带的数量和起止频率范围。
public var frequencyBands: Int = 80 //频带数量
public var startFrequency: Float = 100 //起始频率
public var endFrequency: Float = 18000 //截止频率
现在可以根据这几个属性确定新的频带:
private lazy var bands: [(lowerFrequency: Float, upperFrequency: Float)] = {
var bands = [(lowerFrequency: Float, upperFrequency: Float)]()
//1:根据起止频谱、频带数量确定增长的倍数:2^n
let n = log2(endFrequency/startFrequency) / Float(frequencyBands)
var nextBand: (lowerFrequency: Float, upperFrequency: Float) = (startFrequency, 0)
for i in 1...frequencyBands {
//2:频带的上频点是下频点的2^n倍
let highFrequency = nextBand.lowerFrequency * powf(2, n)
nextBand.upperFrequency = i == frequencyBands ? endFrequency : highFrequency
bands.append(nextBand)
nextBand.lowerFrequency = highFrequency
}
return bands
}()
接着创建函数findMaxAmplitude
用来计算新频带的值,采用的方法是找出落在该频带范围内的原始振幅数据的最大值:
private func findMaxAmplitude(for band:(lowerFrequency: Float, upperFrequency: Float), in amplitudes: [Float], with bandWidth: Float) -> Float {
let startIndex = Int(round(band.lowerFrequency / bandWidth))
let endIndex = min(Int(round(band.upperFrequency / bandWidth)), amplitudes.count - 1)
return amplitudes[startIndex...endIndex].max()!
}
这样就可以通过新的analyse
函数接收音频原始数据并向外提供加工好的频谱数据:
func analyse(with buffer: AVAudioPCMBuffer) -> [[Float]] {
let channelsAmplitudes = fft(buffer)
var spectra = [[Float]]()
for amplitudes in channelsAmplitudes {
let spectrum = bands.map {
findMaxAmplitude(for: $0, in: amplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize))
}
spectra.append(spectrum)
}
return spectra
}
动画绘制
看上去数据都处理好了,让我们捋一捋袖子开始绘制动画了!打开自定义视图SpectrumView
文件,首先创建两个CAGradientLayer
:
var leftGradientLayer = CAGradientLayer()
var rightGradientLayer = CAGradientLayer()
新建函数setupView()
,分别设置它们的colors
和locations
属性,这两个属性分别决定渐变层的颜色和位置,再将它们添加到视图的