内容简介:一步一步教你实现iOS音频频谱动画(一)一步一步教你实现iOS音频频谱动画(二)[未发布]
一步一步教你实现iOS音频频谱动画(一)
一步一步教你实现iOS音频频谱动画(二)[未发布]
基于篇幅考虑,本次教程分为两篇文章,本篇文章主要讲述音频播放和频谱数据的获取,下篇将讲述数据处理和动画绘制。
前言
很久以前在电脑上听音乐的时候,经常会调出播放器的一个小工具,里面的柱状图会随着音乐节奏而跳动,就感觉自己好专业,尽管后来才知道这个是音频信号在频域下的表现。
热身知识
动手写代码之前,让我们先了解几个基础概念吧
音频数字化
-
采样:总所周知,声音是一种压力波,是连续的,然而在计算机中无法表示连续的数据,所以只能通过间隔采样的方式进行离散化,其中采集的频率称为采样率。根据奈奎斯特采样定理 ,当采样率大于信号最高频率的2倍时信号频率不会失真。人类能听到的声音频率范围是20hz到20khz,所以CD等采用了44.1khz采样率能满足大部分需要。
-
量化:每次采样的信号强度也会有精度的损失,如果用16位的Int(位深度)来表示,它的范围是[-32768,32767],因此位深度越高可表示的范围就越大,音频质量越好。
-
声道数:为了更好的效果,声音一般采集左右双声道的信号,如何编排呢?一种是采用交错排列(Interleaved):
LRLRLRLR
,另一种采用各自排列(non-Interleaved):LLL RRR
。
以上将模拟信号数字化的方法称为脉冲编码调制(PCM),而本文中我们就需要对这类数据进行加工处理。
傅里叶变换
现在我们的音频数据是时域的,也就是说横轴是时间,纵轴是信号的强度,而动画展现要求的横轴是频率。将信号从时域转换成频域可以使用傅里叶变换实现,信号经过傅里叶变换分解成了不同频率的正弦波,这些信号的频率和振幅就是我们需要实现动画的数据。
图1 (fromnti-audio) 傅里叶变换将信号从时域转换成频域实际上计算机中处理的都是离散傅里叶变换(DFT),而快速傅里叶变换(FFT)是快速计算离散傅里叶变换(DFT)或其逆变换的方法,它将DFT的复杂度从O(n²)降低到O(nlogn)。 如果你刚才点开前面链接看过其中介绍的FFT算法,那么可能会觉得这FFT代码该怎么写?不用担心,苹果为此提供了Accelerate框架,其中vDSP部分提供了数字信号处理的函数实现,包含FFT。有了vDSP,我们只需几个步骤即可实现FFT,简单便捷,而且性能高效。
iOS中的音频框架
现在开始让我们看一下iOS系统中的音频框架, AudioToolbox
功能强大,不过提供的API都是基于 C语言 的,其大多数功能已经可以通过 AVFoundation
实现,它利用 Objective-C
/ Swift
对于底层接口进行了封装。我们本次需求比较简单,只需要播放音频文件并进行实时处理,所以 AVFoundation
中的 AVAudioEngine
就能满足本次音频播放和处理的需要。
AVAudioEngine
AVAudioEngine
从iOS8加入到 AVFoundation
框架,它提供了以前需要深入到底层 AudioToolbox
才实现的功能,比如实时音频处理。它把音频处理的各环节抽象成 AVAudioNode
并通过 AVAudioEngine
进行管理,最后将它们连接构成完整的节点图。以下就是本次教程的 AVAudioEngine
与其节点的连接方式。
图3 AVAudioEngine和AVAudioNode连接图
mainMixerNode
和 outputNode
都是在被访问的时候默认由 AVAudioEngine
对象创建并连接的单例对象,也就是说我们只需要手动创建 engine
和 player
节点并将他们连接就可以了!最后在 mainMixerNode
的输出总线上安装分接头将定量输出的 AVAudioPCMBuffer
数据进行转换和FFT。
代码实现
了解了以上相关知识后,我们就可以开始编写代码了。打开项目 AudioSpectrum01
,首先来完成音频播放的功能。
音频播放
在 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()
语句下方输入代码:
engine.mainMixerNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(fftSize), format: nil, block: { (buffer, when) in if !self.player.isPlaying { return } buffer.frameLength = AVAudioFrameCount(self.fftSize) let magnitudes = self.fft(buffer) if self.delegate != nil { self.delegate?.player(self, didGenerateSpectrum: magnitudes) } }) 复制代码
在 block 中将拿到的2048个frame的buffer交由 fft
函数进行计算。
按照44100hz采样率和1Frame=1Packet来计算(可以参考这里关于channel、sample、frame、packet的概念与关系),那么block将会在一秒中调用44100/2048≈21.5次左右,另外需要注意的是block有可能不在主线程调用。
FFT实现
终于到实现 FFT
的时候了,根据 vDSP
文档,首先需要定义一个 FFT
的 权重数组(fftSetup)
,它可以在多次 FFT
中重复使用和提升 FFT
性能:
private lazy var fftSetup = vDSP_create_fftsetup(vDSP_Length(round(log2(Double(frameLength)))), 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
的。
图4 interleaved的样本数据需要进行deinterleave
-
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]},与输入位数一致。
图5 第一次转换时,vDSP_ctoz函数将实数数组转换成DSPSplitComplex结构
最后在通过 FFT
函数计算出结果后,利用委托通知 ViewController
产生了新的数据。再次运行项目,不过现在除了听到音乐外,我们目前只能打印出数据解解馋。
图6 将结果通过Google Sheets绘制出来的频谱图
好了,本篇文章内容到这里就结束了,下一篇文章将最对数据进行处理和动画绘制。
资料参考
[1] wikipedia,脉冲编码调制, zh.wikipedia.org/wiki/脈衝編碼調變
[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…
以上所述就是小编给大家介绍的《一步一步教你实现iOS音频频谱动画(一)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 一步一步教你实现iOS音频频谱动画(二)
- 一步一步教你实现iOS音频频谱动画(一)
- 台湾5G频谱竞标金额破千亿大关,五大运营商掏空家底
- 声网 Agora 音频互动 MoS 分方法:为音频互动体验进行实时打分
- 用音频引导玩家 详解VR空间音频的重要性及使用方法
- 用音频引导玩家 详解VR空间音频的重要性及使用方法
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。