Android Visualizer
系统Visualizer
提供了方便的 api 来获取播放音频的波形或 FFT 数据,一般使用方式是:
用 audio session ID 创建Visualizer
对象,传 0 可获取混音后的可视化数据,传特定播放器或AudioTrack
所使用的 audio session 的
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
ID,可获取它们所播放音频的可视化数据
2. 调setCaptureSize
方法设置每次获取的数据大小,调setDataCaptureListener
方法设置数据回调并指定获取数据频率(即回调频率)和数据类型(波形或 FFT)
3. 调setEnabled
方法开始获取数据,不再需要时调release
方法释放资源
更详细的 api 信息可查看官方文档。
系统Visualizer
输出的数据大小正比于音量,当音量为 0 时,输出也为 0,可视化效果会随音量变化。
使用系统Visualizer
存在兼容性问题,在有些机型上会导致系统音效失效,如要在所有机型上都能无副作用地展示动效,需要实现自定义Visualizer
。
自定义 Visualizer
作为跟系统Visualizer
功能一致的数据源,自定义 Visualizer 需具备两个功能:
获取 pcm 数据,计算 FFT以指定频率和大小发送 FFT 数据
实现第一个功能首先要获取播放音频的 pcm 数据,这要求使用的播放器能够提供 pcm 数据,我们的播放器是自己实现的,能够满足这个要求。我们对播放器进行了扩展,增加了收集解码后的 pcm 数据计算 FFT 的功能。
由于不同音频采样率不同,而计算 FFT 时采用固定的窗口大小,导致 FFT 计算结果回调频率随播放音频改变,同时指定的数据大小可能跟计算结果的大小不同,因此要实现第二个功能,需要对计算结果做固定频率和采样等处理。
另外,我们的播放器在播放进程中运行,而实际使用 FFT 数据的动效页面运行于主进程中,所以还需要跨进程传输数据。
综上,自定义 Visualizer 的整体流程是:在播放进程 native 层中计算 FFT,通过 JNI 调用,把计算结果回调给Java 层,然后通过 AIDL 把 FFT 数据传递给主进程进行后续的数据处理和发送操作。如下图所示:
固定频率需要将可变的 FFT 计算结果回调频率转换为外部设置的 Visualizer 回调频率,如下图所示:
根据所需数据发送时间间隔和 FFT 回调时间间隔差值的不同,我们采用两种不同的方式。
当时间间隔差值大于回调时间间隔时,每 t1/tt1/t 次回调发送一次数据,其中 t1 为所需数据发送时间间隔,t 为 FFT 回调时间间隔,如下图所示:
采样就是当外部设置的数据大小小于 FFT 计算结果的数据大小时,对原始 FFT 数据以合适的间隔抽取数据,以满足设置的要求。
为了让自定义 Visualizer 返回数据的取值范围跟系统 Visualizer 一致,从而实现数据源无缝切换,我们需要对 FFT 数据进行缩放。这里就需要用到前面提到的模与振幅的计算了,解码所得 pcm 数据的取值范围为 [-1, 1],所以原始信号振幅取值范围为 [0, 1],即 2M/N2M/N2M/N 的取值范围为 [0, 1](绘制时不会用到直流分量,这里不考虑);而系统 Visualizer 返回的 FFT 数据是一个 byte 数组,实部和虚部的取值范围为 [-128, 128],模的取值范围为 [0,128×2][0, 128 \times \sqrt2][0,128×2],那么 2M/N×128×22M/N \times 128 \times \sqrt22M/N×128×2 的取值范围跟系统 Visualizer 输出 FFT 的模的取值范围一致。由于绘制不会用到相位信息,我们可以将用上述方式缩放后的值作为输出 FFT 数据的实部,并把虚部设为 0。
由于数据发送的频率较高,为了避免频繁创建对象导致内存抖动,我们采用对象池来保存数据数组对象,每次从对象池中获取所需大小的数组对象,填充采样数据后加入到队列中等待发送,数据消费完后将数组对象返回到对象池中。
数据处理
不同动效的具体数据处理方式不同,忽略细节上的差异,云音乐现有的动效中,除了宇宙尘埃和孤独星球,其他的处理流程基本一致,如下图所示:
首先根据动效选择的频率范围计算所需的频率数据在 FFT 数组中的索引位置:
然后根据动效所需数据点数,对频率范围内的 FFT 数据进行采样或用一个 FFT 数据表示多个数据点。
然后计算分贝:
其中 M 为 FFT 数据的模。
然后将分贝转化为高度:
其中 MAX_DB 是预设的分贝最大值,maxHeight 是当前动效要求的最大高度。
最后对计算出的高度做数据上的平滑处理。
平滑
对最终用来绘制的数据做平滑处理,可以得到更柔和的曲线,达到更好的视觉效果,如下图所示:
数据平滑算法有很多,我们综合考虑效果和计算复杂度选择了 Savitzky–Golay 滤波法,其计算方式如下,对应的窗口大小分别为5、7 和 9,可以按需选择不同的窗口大小。
经过平滑处理后数据的变化如下图所示:
BufferQueue
有些动效的数据处理计算比较复杂,为提升并行性,减少主线程耗时,我们借鉴系统图形框架中 BufferQueue 的思想,实现了一个简单的承载动效绘制数据,连接数据处理和绘制的 BufferQueue,其工作过程如下图所示:
在使用BufferQueue
的动效绘制类初始化时,根据需要创建一个合适大小的BufferQueue
,并启动用于执行数据处理的Looper
线程。
数据处理部分对应BufferQueue
的Producer
,当 FFT 数据到来时,通过绑定Looper
线程的Handler
将数据发送到Looper
线程中执行数据处理。数据处理时,首先调用Producer
的dequeue
方法从BufferQueue
中获取空闲的Buffer
,然后对 FFT 数据进行处理,生成需要的数据向Buffer
中填充,最后调用Producer
的queue
方法将Buffer
加入到BufferQueue
中的 queued 队列中。
绘制部分对应BufferQueue
的Consumer
,调用Producer
的queue
方法时会触发ConsumerListener
的onBufferAvailable
回调,在回调中通过绑定主线程的Handler
切换到主线程消费Buffer
。首先调用Consumer
的acquire
方法从BufferQueue
的queued
队列中获取Buffer
,然后从Buffer
中取出所需数据来绘制,最后调用Consumer
的release
方法将上次的Buffer
返回给BufferQueue
。
eue的
queued队列中获取
Buffer,然后从
Buffer中取出所需数据来绘制,最后调用
Consumer的
release方法将上次的
Buffer返回给
BufferQueue`。