听音乐时,有时你会看到那些视觉上令人愉悦的跃动条,它们音量越大跳得越高。通常,左边的条形对应的频率较低(低音),而右边的条形对应较高的频率(高音):
这些跃动条通常被称为视觉均衡器或可视化器,若想在Android应用中展示类似的可视化效果,你可以使用Android原生的Visualizer类,它是Android框架中的一部分,且能够附加到你的AudioTrack。
它是切实有效的,但有一个重要的缺陷:它需要申请 麦克风权限 ,而从官方文档上来看,这是有确切考虑的:
- To protect privacy of certain audio data (e.g voice mail) the use of the visualizer requires the permission. 为了保护某些音频数据(例如语音邮件)的隐私,使用 Visualizer 需要获取权限。
问题是,用户不会允许音乐APP申请使用他们的麦克风权限(这毫无疑问)。而当我翻遍了Android官方提供的API或者其他三方库,却找不到实现这样可视化器效果的替代方案。
因此我考虑自己造轮子,第一个问题是,我需要思考如何将正在播放的音乐,转换成每个跳跃条对应的高度。
可视化器的工作原理
首先,让我们从输入开始。当数字化音频时,我们通常会对信号幅度进行非常频繁的采样,这称为脉冲编码调制 (PCM)。振幅随之被量化,我们将其表示到我们自己的数字标度上。
举个例子,如果编码是PCM-16,这个比例将是16 bit,我们可在2的16次幂的数字范围内表示一个幅度,即65536个不同的幅度值。
如果您在多个channel上采样(如立体声,分别录制左右声道),这些幅度会相互跟随,因此首先是 channel 0 的幅度,然后是 channel 1 的幅度,然后是 channel 0,依此类推。一旦我们获得了这些幅度值作为原始数据,我们就可以继续下一步。为此,我们需要了解声音实际上是什么:
我们听到的声音是物体振动的结果。例如人的声带、吉他的金属弦和木琴身。一般情况下,若不受特定声音振动的影响,空气分子会随机移动。选自《 Digital Sound and Music 》
当敲击音叉时,它会以非常特定的440次/秒 (Hz) 振动,这种振动将通过空气传播到耳膜,在那里以相同的频率共振,大脑会将其解释为音符A。
在PCM中,这可以表示为正弦波,每秒重复440次。这些波的高度不会改变音符,但它们代表振幅;通俗点说,就是当听到它时,你耳朵里的响度。
但是当听音乐时,通常不仅有正在听的音符A(虽然我希望这样),而且还有过多的乐器和声音,从而导致PCM图形对人眼没有意义。实际上它是不同频率和振幅的不同正弦波大量振动的组合。
即使是非常简单的PCM信号(例如方波)在解构为不同的正弦波时也非常复杂:
方波解构为近似正弦和余弦波。
幸运的是,我们有算法来进行这种解构,我们称之为傅立叶变换 。正如上文可视化器所展示的,它实际上是从正弦波和余弦波的组合中解构而出的。余弦基本上是一个 延迟 的正弦波,但是在这个算法中拥有它们非常有用,否则我们将无法为点0创建一个值,因为每个正弦波都是从0开始的,相乘仍然会得到0。
执行傅里叶变换的算法之一是 快速傅里叶变换 (FFT)。在我们的PCM声音数据上运行此FFT算法时,我们将获得每个正弦波的幅度列表。这些波是声音的频率。在列表的开头,我们可以找到低频(低音),最后是高频(高音)。
这样,我们通过绘制一个这样的条形图,其高度由每个频率的幅度决定——我们得到了我们想要的可视化器。
技术实现
现在回到Android。首先,我们需要音频的PCM数据。为此,我们可以将AudioProcessor配置给到我们的ExoPlayer实例,它会在转发之前接收每个音频字节。您还可以进行修改,例如更改幅度或过滤通道,但不是现在。
- privatevalfftAudioProcessor=FFTAudioProcessor()
- valrenderersFactory=object:DefaultRenderersFactory(this){
-
overridefunbuildAudioProcessors():Array
{ - valprocessors=super.buildAudioProcessors()
- returnprocessors+fftAudioProcessor
- }
- }
- player=ExoPlayerFactory.newSimpleInstance(this,renderersFactory,DefaultTrackSelector())
在queueInput(inputBuffer: ByteBuffer)方法中,我们将收到捆在一起作为一帧的byte数据。
这些 byte 可能来自多个 channel,为此我取了所有 channel 的平均值,并且仅将其转发以进行处理。
为了使用傅立叶变换,我使用了Noise库。变换需要一个具有给定样本大小的float列表。样本大小应该是2的因子,我选择了4096。
增加这个数字可获得更精细的数据,但计算时间更长,计算也更不频繁(因为可针对每 X 字节的声音数据进行一次更新,其中X是样本大小)。如果数据是PCM-16,则2个字节构成一个幅度。浮点值并不重要,因为它们可以缩放。如果您提交一个介于0和1之间的数字,则结果都将介于0和1之间(因为无需将正弦波幅度与更高的数字相乘)。
所得结果也将是一个float列表。我们可使用这些频率立即绘制 4096 个条形图,但这不切实际。
来看看如何改进这些结果数据。
频段
首先,我们可以将这些频率组合成更小的组。因此,假设我们将0-20kHz频谱划分为20个小节,每个小节跨越1kHz。
20条比4096条更容易绘制,我们也无需那么多条。如果现在绘制这些值,可以看到,只有最左边的部分在大幅度移动。
这是因为音乐中频率的适用范围大约是20-5000Hz,而听10kHz的声音会让人很烦躁。若将音乐中的较高频率排除在外,你会注意到,它听起来会越来越沉闷,但与较低频率相比,这些频率的幅度非常小。
如果你看过录音室均衡器,则会发现频段也分布不均,频率的下半部分通常占用80-90%的频段:
鉴于此,建议通过为较低频率分配更多频带来使这些频带具有可变宽度。下图是这样做的效果,它看起来会好一些:
似乎不错,但仍然存在2个问题:
首先,右边的频率似乎移动得有点太多了。这是因为我们的采样并不完美,它引入了称为频谱泄漏的伪像,其中原始频率会涂抹到相邻的频率中。为了减少这种拖尾现象,我们可以应用一个窗口函数,在那里我们突出我们感兴趣的频率并调低其他频率。这些窗口有不同类型,但我将使用汉明窗口 (Hamming-window)。我们感兴趣的频率是中间的频段,并针对两端进行抑制:
最后,还有一个小问题,这在上面的 gif 中无法体现,但当听音乐时会立即注意到:条跃动太早了,它们在你意料不到的时候跃动起来。
意外缓冲区
这种不同步的行为是因为在ExoPlayer AudioProcessor中,我们在数据传递到AudioTrack之前接收到数据,而AudioTrack有自己的缓冲区,这会导致视觉效果领先于音频效果,导致延迟输出。
对此的解决方案是将ExoPlayer缓冲区大小计算部分的代码进行复制,因此我的AudioProcessor中的缓冲区大小与AudioTrack完全相同。
我将传入的字节放在缓冲区的末尾,只处理缓冲区开头的字节(FIFO 队列),我如愿以偿延迟了 FFT。
最终效果
我创建了一个代码仓库,这上面,我通过播放在线广播并使用我创建的可视化器进行绘图,以展示我的 FFT 处理器。它肯定不能直接用于线上产品,但如果您正在为音乐APP寻找可视化工具,它会提供一个很好的基础。
原文地址:https://juejin.cn/post/6996076172714967071