龙口建网站价格,电子电工技术工程网,网站开发三层架构,东莞企业网站seo本文是系列文章中的第二篇#xff0c;上篇讲述了音频播放和频谱数据计算#xff0c;本篇讲述数据处理和动画的绘制。前言在上篇文章中我们已经拿到了频谱数据#xff0c;也知道了数组每个元素表示的是振幅#xff0c;那这些数组元素之间有什么关系呢#xff1f;根据FFT的原…本文是系列文章中的第二篇上篇讲述了音频播放和频谱数据计算本篇讲述数据处理和动画的绘制。前言在上篇文章中我们已经拿到了频谱数据也知道了数组每个元素表示的是振幅那这些数组元素之间有什么关系呢根据FFT的原理 N个音频信号样本参与计算将产生N/2个数据(2048/21024)其频率分辨率△fFs/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^nlet 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 : highFrequencybands.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文件首先创建两个CAGradientLayervar leftGradientLayer CAGradientLayer()var rightGradientLayer CAGradientLayer()新建函数setupView()分别设置它们的colors和locations属性这两个属性分别决定渐变层的颜色和位置再将它们添加到视图的layer层中它们将承载左右两个声道的动画。private func setupView() {rightGradientLayer.colors [UIColor.init(red: 52/255, green: 232/255, blue: 158/255, alpha: 1.0).cgColor,UIColor.init(red: 15/255, green: 52/255, blue: 67/255, alpha: 1.0).cgColor]rightGradientLayer.locations [0.6, 1.0]self.layer.addSublayer(rightGradientLayer)leftGradientLayer.colors [UIColor.init(red: 194/255, green: 21/255, blue: 0/255, alpha: 1.0).cgColor,UIColor.init(red: 255/255, green: 197/255, blue: 0/255, alpha: 1.0).cgColor]leftGradientLayer.locations [0.6, 1.0]self.layer.addSublayer(leftGradientLayer)}接着在View的初始化函数init(frame: CGRect) 和 init?(coder aDecoder: NSCoder)中调用它以便在代码或者Storyboard中创建SpectrumView时都可以正确地进行初始化。override init(frame: CGRect) {super.init(frame: frame)setupView()}required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder)setupView()}关键的来了定义一个spectra属性对外接收频谱数据并通过属性观察didSet创建两个声道的柱状图的UIBezierPath经过CAShapeLayer包装后应用到各自CAGradientLayer的mask属性中就得到了渐变的柱状图效果。var spectra:[[Float]]? {didSet {if let spectra spectra {// left channellet leftPath UIBezierPath()for (i, amplitude) in spectra[0].enumerated() {let x CGFloat(i) * (barWidth space) spacelet y translateAmplitudeToYPosition(amplitude: amplitude)let bar UIBezierPath(rect: CGRect(x: x, y: y, width: barWidth, height: bounds.height - bottomSpace - y))leftPath.append(bar)}let leftMaskLayer CAShapeLayer()leftMaskLayer.path leftPath.cgPathleftGradientLayer.frame CGRect(x: 0, y: topSpace, width: bounds.width, height: bounds.height - topSpace - bottomSpace)leftGradientLayer.mask leftMaskLayer// right channelif spectra.count 2 {let rightPath UIBezierPath()for (i, amplitude) in spectra[1].enumerated() {let x CGFloat(spectra[1].count - 1 - i) * (barWidth space) spacelet y translateAmplitudeToYPosition(amplitude: amplitude)let bar UIBezierPath(rect: CGRect(x: x, y: y, width: barWidth, height: bounds.height - bottomSpace - y))rightPath.append(bar)}let rightMaskLayer CAShapeLayer()rightMaskLayer.path rightPath.cgPathrightGradientLayer.frame CGRect(x: 0, y: topSpace, width: bounds.width, height: bounds.height - topSpace - bottomSpace)rightGradientLayer.mask rightMaskLayer}}}}其中translateAmplitudeToYPosition函数的作用是将振幅转换成视图坐标系中的Y值private func translateAmplitudeToYPosition(amplitude: Float) - CGFloat{let barHeight: CGFloat CGFloat(amplitude) * (bounds.height - bottomSpace - topSpace)return bounds.height - bottomSpace - barHeight}回到ViewController在SpectrumPlayerDelegate的方法中直接将接收到的数据交给spectrumView:// MARK: SpectrumPlayerDelegateextension ViewController: AudioSpectrumPlayerDelegate {func player(_ player: AudioSpectrumPlayer, didGenerateSpectrum spectra: [[Float]]) {DispatchQueue.main.async {//1: 将数据交给spectrumViewself.spectrumView.spectra spectra}}}敲了这么多代码终于可以运行一下看看效果了额...看上去效果好像不太妙啊。请放心喝杯咖啡放松一下待会一个一个来解决。图2 初始动画效果调整优化效果不好主要体现在这三点1)动画与音乐节奏匹配度不高2)画面锯齿过多 3)动画闪动明显。 首先来解决第一个问题:节奏匹配匹配度不高的一部分原因是目前的动画幅度太小了特别是中高频部分。我们先放大个5倍看看效果修改analyse函数func analyse(with buffer: AVAudioPCMBuffer) - [[Float]] {let channelsAmplitudes fft(buffer)var spectra [[Float]]()for amplitudes in channelsAmplitudes {let spectrum bands.map {//1: 直接在此函数调用后乘以5findMaxAmplitude(for: $0, in: amplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize)) * 5}spectra.append(spectrum)}return spectra}图3 幅度放大5倍之后低频部分都超出画面了低频部分的能量相比中高频大许多但实际上低音听上去并没有那么明显这是为什么呢这里涉及到响度的概念响度(loudness又称音响或音量)是与声强相对应的声音大小的知觉量。声强是客观的物理量响度是主观的心理量。响度不仅跟声强有关还跟频率有关。不同频率的纯音在和1000Hz某个声压级纯音等响时其声压级也不相同。这样的不同声压级作为频率函数所形成的曲线称为等响度曲线。改变这个1000Hz纯音的声压级可以得到一组等响度曲线。最下方的0方曲线表示人类能听到的最小的声音响度即听阈最上方是人类能承受的最大的声音响度即痛阈。图4 横坐标为频率纵坐标为声压级波动的一条条曲线就是等响度曲线(equal-loudness contours)这些曲线代表着声音的频率和声压级在相同响度级中的关联。原来人耳对不同频率的声音敏感度不同两个声音即使声压级相同如果频率不同那感受到的响度也不同。基于这个原因需要采用某种频率计权来模拟使得像人耳听上去的那样。常用的计权方式有A、B、C、D等A计权最为常用对低频部分相比其他计权有着最多的衰减这里也将采用A计权。图5 蓝色曲线就是A计权是根据40 phon的等响曲线模拟出来的反曲线在RealtimeAnalyzer类中新建函数createFrequencyWeights()它将返回A计权的系数数组private func createFrequencyWeights() - [Float] {let Δf 44100.0 / Float(fftSize)let bins fftSize / 2 //返回数组的大小var f (0..f f.map { $0 * $0 }let c1 powf(12194.217, 2.0)let c2 powf(20.598997, 2.0)let c3 powf(107.65265, 2.0)let c4 powf(737.86223, 2.0)let num f.map { c1 * $0 * $0 }let den f.map { ($0 c2) * sqrtf(($0 c3) * ($0 c4)) * ($0 c1) }let weights num.enumerated().map { (index, ele) inreturn 1.2589 * ele / den[index]}return weights}更新analyse函数中的代码func analyse(with buffer: AVAudioPCMBuffer) - [[Float]] {let channelsAmplitudes fft(buffer)var spectra [[Float]]()//1: 创建权重数组let aWeights createFrequencyWeights()for amplitudes in channelsAmplitudes {//2原始频谱数据依次与权重相乘let weightedAmplitudes amplitudes.enumerated().map {(index, element) inreturn element * aWeights[index]}let spectrum bands.map {//3: findMaxAmplitude函数将从新的weightedAmplitudes中查找最大值findMaxAmplitude(for: $0, in: weightedAmplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize)) * 5}spectra.append(spectrum)}return spectra}再次运行项目看看效果好多了是吗图6 A计权之后的动画表现锯齿消除接着是锯齿过多的问题手段是将相邻较长的拉短较短的拉长常见的办法是使用加权平均。创建函数highlightWaveform()private func highlightWaveform(spectrum: [Float]) - [Float] {//1: 定义权重数组数组中间的5表示自己的权重// 可以随意修改个数需要奇数let weights: [Float] [1, 2, 3, 5, 3, 2, 1]let totalWeights Float(weights.reduce(0, ))let startIndex weights.count / 2//2: 开头几个不参与计算var averagedSpectrum Array(spectrum[0..for i in startIndex..//3: zip作用: zip([a,b,c], [x,y,z]) - [(a,x), (b,y), (c,z)]let zipped zip(Array(spectrum[i - startIndex...i startIndex]), weights)let averaged zipped.map { $0.0 * $0.1 }.reduce(0, ) / totalWeightsaveragedSpectrum.append(averaged)}//4末尾几个不参与计算averagedSpectrum.append(contentsOf: Array(spectrum.suffix(startIndex)))return averagedSpectrum}analyse函数需要再次更新func analyse(with buffer: AVAudioPCMBuffer) - [[Float]] {let channelsAmplitudes fft(buffer)var spectra [[Float]]()for amplitudes in channelsAmplitudes {let weightedAmplitudes amplitudes.enumerated().map {(index, element) inreturn element * weights[index]}let spectrum bands.map {findMaxAmplitude(for: $0, in: weightedAmplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize)) * 5}//1: 添加到数组之前调用highlightWaveformspectra.append(highlightWaveform(spectrum: spectrum))}return spectra}图7 锯齿少了波形变得明显闪动优化动画闪动给人的感觉就好像丢帧一样。造成这个问题的原因是因为频带的值前后两帧变化太大我们可以将上一帧的值缓存起来然后跟当前帧的值进行...没错又是加权平均 (⊙﹏⊙)b 继续开始编写代码首先需要定义两个属性//缓存上一帧的值private var spectrumBuffer: [[Float]]?//缓动系数数值越大动画越缓public var spectrumSmooth: Float 0.5 {didSet {spectrumSmooth max(0.0, spectrumSmooth)spectrumSmooth min(1.0, spectrumSmooth)}}接着修改analyse函数func analyse(with buffer: AVAudioPCMBuffer) - [[Float]] {let channelsAmplitudes fft(buffer)let aWeights createFrequencyWeights()//1: 初始化spectrumBufferif spectrumBuffer.count 0 {for _ in 0..spectrumBuffer.append(Array(repeating: 0, count: frequencyBands))}}//2: index在给spectrumBuffer赋值时需要用到for (index, amplitudes) in channelsAmplitudes.enumerated() {let weightedAmp amplitudes.enumerated().map {(index, element) inreturn element * aWeights[index]}var spectrum bands.map {findMaxAmplitude(for: $0, in: weightedAmplitudes, with: Float(buffer.format.sampleRate) / Float(self.fftSize)) * 5}spectrum highlightWaveform(spectrum: spectrum)//3: zip用法前面已经介绍过了let zipped zip(spectrumBuffer[index], spectrum)spectrumBuffer[index] zipped.map { $0.0 * spectrumSmooth $0.1 * (1 - spectrumSmooth) }}return spectrumBuffer}再次运行项目得到最终效果结尾音频频谱的动画实现到此已经全部完成。本人之前对音频和声学毫无经验两篇文章涉及的方法理论均参考自互联网肯定有不少错误欢迎指正。参考资料[1] 维基百科, 倍频程频带, en.wikipedia.org/wiki/Octave…[2] 维基百科, 响度, zh.wikipedia.org/wiki/%E9%9F…[3] mathworksA-weighting Filter with Matlabwww.mathworks.com/matlabcentr…[4] 动画效果网易云音乐APP、MOO音乐APP。感兴趣的同学可以用卡农钢琴版音乐和这两款APP进行对比^_^会发现区别。作者potato04链接https://juejin.im/post/5c26d44ae51d45619a4b8b1e