一步一步教你实现iOS音频频谱动画(一)

栏目: IOS · 发布时间: 5年前

内容简介:一步一步教你实现iOS音频频谱动画(一)一步一步教你实现iOS音频频谱动画(二)[未发布]

一步一步教你实现iOS音频频谱动画(一)

一步一步教你实现iOS音频频谱动画(二)[未发布]

示例代码下载

基于篇幅考虑,本次教程分为两篇文章,本篇文章主要讲述音频播放和频谱数据的获取,下篇将讲述数据处理和动画绘制。

前言

很久以前在电脑上听音乐的时候,经常会调出播放器的一个小工具,里面的柱状图会随着音乐节奏而跳动,就感觉自己好专业,尽管后来才知道这个是音频信号在频域下的表现。

热身知识

动手写代码之前,让我们先了解几个基础概念吧

音频数字化

  • 采样:总所周知,声音是一种压力波,是连续的,然而在计算机中无法表示连续的数据,所以只能通过间隔采样的方式进行离散化,其中采集的频率称为采样率。根据奈奎斯特采样定理 ,当采样率大于信号最高频率的2倍时信号频率不会失真。人类能听到的声音频率范围是20hz到20khz,所以CD等采用了44.1khz采样率能满足大部分需要。

  • 量化:每次采样的信号强度也会有精度的损失,如果用16位的Int(位深度)来表示,它的范围是[-32768,32767],因此位深度越高可表示的范围就越大,音频质量越好。

  • 声道数:为了更好的效果,声音一般采集左右双声道的信号,如何编排呢?一种是采用交错排列(Interleaved): LRLRLRLR ,另一种采用各自排列(non-Interleaved): LLL RRR

以上将模拟信号数字化的方法称为脉冲编码调制(PCM),而本文中我们就需要对这类数据进行加工处理。

傅里叶变换

现在我们的音频数据是时域的,也就是说横轴是时间,纵轴是信号的强度,而动画展现要求的横轴是频率。将信号从时域转换成频域可以使用傅里叶变换实现,信号经过傅里叶变换分解成了不同频率的正弦波,这些信号的频率和振幅就是我们需要实现动画的数据。

一步一步教你实现iOS音频频谱动画(一)
图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 就能满足本次音频播放和处理的需要。

一步一步教你实现iOS音频频谱动画(一)
图2 (fromWWDC16) iOS/MAC OS X 音频技术栈

AVAudioEngine

AVAudioEngine 从iOS8加入到 AVFoundation 框架,它提供了以前需要深入到底层 AudioToolbox 才实现的功能,比如实时音频处理。它把音频处理的各环节抽象成 AVAudioNode 并通过 AVAudioEngine 进行管理,最后将它们连接构成完整的节点图。以下就是本次教程的 AVAudioEngine 与其节点的连接方式。

一步一步教你实现iOS音频频谱动画(一)

图3 AVAudioEngine和AVAudioNode连接图

mainMixerNodeoutputNode 都是在被访问的时候默认由 AVAudioEngine 对象创建并连接的单例对象,也就是说我们只需要手动创建 engineplayer 节点并将他们连接就可以了!最后在 mainMixerNode 的输出总线上安装分接头将定量输出的 AVAudioPCMBuffer 数据进行转换和FFT。

代码实现

了解了以上相关知识后,我们就可以开始编写代码了。打开项目 AudioSpectrum01 ,首先来完成音频播放的功能。

音频播放

AudioSpectrumPlayer 类中创建 AVAudioEngineAVAudioPlayerNode 两个实例变量:

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 上,再把 playerenginemainMixerNode 连接起来就完成了 AVAudioEngine 的整个节点图创建(详见图3)。

//2 :在调用 enginestrat() 方法开始启动 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 :停止播放调用 playerstop() 方法即可。

音频播放代码已经完成,运行项目,试试点击音乐右侧的 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 计算的步骤了,不过以下两点是我在完成这一部分内容过程中比较难理解的部分:

  1. 通过 buffer 对象的方法 floatChannelData 获取样本数据,如果是多声道并且是 interleaved ,我们就需要对它进行 deinterleave , 通过下图就能比较清楚的知道 deinterleave 前后的结构,不过在我试验了许多音频文件之后,发现都是 non-interleaved 的。
一步一步教你实现iOS音频频谱动画(一)

图4 interleaved的样本数据需要进行deinterleave

  1. 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]},与输入位数一致。

一步一步教你实现iOS音频频谱动画(一)

图5 第一次转换时,vDSP_ctoz函数将实数数组转换成DSPSplitComplex结构

最后在通过 FFT 函数计算出结果后,利用委托通知 ViewController 产生了新的数据。再次运行项目,不过现在除了听到音乐外,我们目前只能打印出数据解解馋。

一步一步教你实现iOS音频频谱动画(一)

图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音频频谱动画(一)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

精通Spring

精通Spring

罗时飞 / 第1版 (2005年4月1日) / 2005-4 / 39.0

本书深入剖析了当前流行的轻量级开发框架Spring技术。本书总共分成3部分。第一部分,重点阐述Spring的架构。这部分内容循序渐进带领开发者进入Spring中。主要在于阐述Spring IoC和Spring AOP。第二部分,重点阐述Spring的使用。这部分内容从简化Java/J2EE的角度出发,从J2EE平台各个技术层面分析、并给出大量的研究实例,对Spring提供的API进行阐述。主要在于......一起来看看 《精通Spring》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具