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

原文連結:
作者:potato04
链接:https://juejin.im/post/5c1bbec66fb9a049cb18b64c
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

image

如果你想先看看最终效果再决定看不看文章 -> bilibili
示例代码下载

第二篇:一步一步教你实现iOS音频频谱动画(二)

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

前言

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

热身知识

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

音频数字化

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

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

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

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

傅里叶变换

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

image

图1 (from nti-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就能满足本次音频播放和处理的需要。

image

图2 (from WWDC16) iOS/MAC OS X 音频技术栈

AVAudioEngine

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

image

图3 AVAudioEngine和AVAudioNode连接图

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

代码实现

了解了以上相关知识后,我们就可以开始编写代码了。打开项目AudioSpectrum01-starter,首先要实现的是音频播放功能。

如果你只是想浏览实现代码,打开项目AudioSpectrum01-final即可,已经完成本篇文章的所有代码

音频播放

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

接下来在init()方法中添加如下代码:

//1:这里将player挂载到engine上,再把playerenginemainMixerNode连接起来就完成了AVAudioEngine的整个节点图创建(详见图3)。
//2:在调用enginestrat()方法开始启动engine之前,需要通过prepare()方法提前分配相关资源

继续完善play(withFileName fileName: String)stop()方法:

//1:首先需要确保文件名为fileName的音频文件能正常加载,然后通过stop()方法停止之前的播放,再调用scheduleFile(_:at:completionHandler:)方法编排新的文件,最后通过play()方法开始播放。
//2:停止播放调用playerstop()方法即可。

音频播放功能已经完成,运行项目,试试点击音乐右侧的Play按钮进行音频播放吧。

音频数据获取

前面提到我们可以在mainMixerNode上安装分接头定量获取AVAudioPCMBuffer数据,现在打开AudioSpectrumPlayer文件,先定义一个属性: fftSize,它是每次获取到的buffer的frame数量。

将光标定位至init()方法中的engine.connect()语句下方,调用mainMixerNodeinstallTap方法开始安装分接头:

在分接头的回调 block 中将拿到的 2048 个 frame 的 buffer 交由fft函数进行计算,最后将计算的结果通过delegate进行传递。

按照 44100hz 采样率和 1 Frame = 1 Packet 来计算(可以参考这里关于channel、sample、frame、packet的概念与关系),那么block将会在一秒中调用44100/2048≈21.5次左右,另外需要注意的是block有可能不在主线程调用。

FFT实现

终于到实现FFT的时候了,根据vDSP文档,首先需要定义一个FFT权重数组(fftSetup),它可以在多次FFT中重复使用和提升FFT性能:

不需要时在析构函数(反初始化函数)中销毁:

最后新建fft函数,实现代码如下:

通过代码中的注释,应该能了解如何从buffer获取音频样本数据并进行FFT计算了,不过以下两点是我在完成这一部分内容过程中比较难理解的部分:

  1. 通过buffer对象的方法floatChannelData获取样本数据,如果是多声道并且是interleaved,我们就需要对它进行deinterleave, 通过下图就能比较清楚的知道deinterleave前后的结构,不过在我试验了许多音频文件之后,发现都是non-interleaved的,也就是无需进行转换。┑( ̄Д  ̄)┍
image

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

image

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

再次运行项目,现在除了听到音乐之外还可以在控制台中看到打印输出的频谱数据。

image

图6 将结果通过Google Sheets绘制出来的频谱图

好了,本篇文章内容到这里就结束了,下一篇文章将对计算好的频谱数据进行处理和动画绘制。

资料参考
[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…

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354