发布时间:2025-06-24 19:20:59 作者:北方职教升学中心 阅读量:293
然后将分贝转化为高度:
h=db/MAX_DB⋅maxHeighth=db/MAX\_DB \cdot maxHeighth=db/MAX_DB⋅maxHeight
其中 MAX_DB 是预设的分贝最大值,maxHeight 是当前动效要求的最大高度。
为了让自定义 Visualizer 返回数据的取值范围跟系统 Visualizer
一致,从而实现数据源无缝切换,我们需要对 FFT 数据进行缩放。
绘制部分对应 BufferQueue
的 Consumer
,调用 Producer
的 queue
方法时会触发 ConsumerListener
的 onBufferAvailable
回调,在回调中通过绑定主线程的 Handler
切换到主线程消费 Buffer
。数据处理时,首先调用 Producer
的 dequeue
方法从 BufferQueue
中获取空闲的 Buffer
,然后对 FFT 数据进行处理,生成需要的数据向 Buffer
中填充,最后调用 Producer
的 queue
方法将 Buffer
加入到 BufferQueue
中的 queued 队列中。
由于 FFT 数据回调的时间间隔大于 16ms,如果只在数据到来时绘制,会产生视觉上的卡顿,为了得到更好的视觉效果,需要在两次回调之间加入过渡帧,以达到渐变的动画效果。本节将从对绘制来说比较重要的体验和性能方面介绍一些动效绘制的优化经验。
当时间间隔差值小于等于回调时间间隔时,每 t/Δtt/ \Delta tt/Δt 次回调丢弃一次数据,其中 t 为 FFT 回调时间间隔,Δt\Delta tΔt 为时间间隔差值,如下图所示:
当时间间隔差值大于回调时间间隔时,每 t1/tt1/tt1/t 次回调发送一次数据,其中 t1 为所需数据发送时间间隔,t 为 FFT 回调时间间隔,如下图所示:
采样就是当外部设置的数据大小小于 FFT 计算结果的数据大小时,对原始 FFT 数据以合适的间隔抽取数据,以满足设置的要求。
综上,自定义 Visualizer 的整体流程是:在播放进程 native 层中计算 FFT,通过 JNI 调用,把计算结果回调给Java 层,然后通过 AIDL 把 FFT 数据传递给主进程进行后续的数据处理和发送操作。
结语
本文介绍了 Android 音频可视化涉及的背景知识和实现过程,并提供了一些问题解决方案和优化思路。7 和 9,可以按需选择不同的窗口大小。
由于数据发送的频率较高,为了避免频繁创建对象导致内存抖动,我们采用对象池来保存数据数组对象,每次从对象池中获取所需大小的数组对象,填充采样数据后加入到队列中等待发送,数据消费完后将数组对象返回到对象池中。
另外,我们的播放器在播放进程中运行,而实际使用 FFT 数据的动效页面运行于主进程中,所以还需要跨进程传输数据。
然后计算分贝:
db=20log10Mdb=20\log_{10}Mdb=20log10M
其中 M 为 FFT 数据的模。本文专注于通用方案,不涉及特定动效的具体实现,希望读者能从中受到些许启发,实现自己的酷炫动效。
数据处理
不同动效的具体数据处理方式不同,忽略细节上的差异,云音乐现有的动效中,除了宇宙尘埃和孤独星球,其他的处理流程基本一致,如下图所示:
首先根据动效选择的频率范围计算所需的频率数据在 FFT 数组中的索引位置:
fr=fs/N,start=⌈MIN/fr⌉,end=⌊MAX/fr⌋f_r=f_s/N, start=\lceil MIN/f_r \rceil, end=\lfloor MAX/f_r \rfloorfr=fs/N,start=⌈MIN/fr⌉,end=⌊MAX/fr⌋
其中 fsf_sfs 为采样率,N 为 FFT 窗口大小,frf_rfr 为频率分辨率,MIN 为频率范围起始值,MAX 为频率范围结束值。
参考资料
- 傅里叶变换
- 离散傅里叶变换
- 快速傅里叶变换
- 数据平滑
供了一些问题解决方案和优化思路。如下图所示:固定频率需要将可变的 FFT 计算结果回调频率转换为外部设置的 Visualizer 回调频率,如下图所示:
根据所需数据发送时间间隔和 FFT 回调时间间隔差值的不同,我们采用两种不同的方式。本文专注于通用方案,不涉及特定动效的具体实现,希望读者能从中受到些许启发,实现自己的酷炫动效。
绘制
绘制部分的主要工作是调用系统
Canvas
API 将处理后的数据绘制成所需的效果,具体如何使用 API 绘制,随动效的不同而不同,这里不展开介绍。同时,我们实现了在不修改上层各动效的数据处理和绘制逻辑的基础上切换数据源,如下图所示:Android Visualizer
系统
Visualizer
提供了方便的 api 来获取播放音频的波形或 FFT 数据,一般使用方式是:- 用 audio session ID 创建
Visualizer
对象,传 0 可获取混音后的可视化数据,传特定播放器或AudioTrack
所使用的 audio session 的 ID,可获取它们所播放音频的可视化数据 - 调
setCaptureSize
方法设置每次获取的数据大小,调setDataCaptureListener
方法设置数据回调并指定获取数据频率(即回调频率)和数据类型(波形或 FFT) - 调
setEnabled
方法开始获取数据,不再需要时调release
方法释放资源
更详细的 api 信息可查看官方文档。
计算公式看起来很复杂,但不懂也不会影响我们实现音频可视化,FFT 的计算可以使用已有的库,不需要自己来实现。实现方式是在两次数据到达的时间间隔内,以上次数据为起点,本次数据为终点,根据当前时间相对于数据到达时间的消逝时间计算当前的高度,不断重复绘制,如下图所示:
性能优化有两大手段:batch 和 cache,在动效绘制时也可以使用这些手段。
使用系统
Visualizer
存在兼容性问题,在有些机型上会导致系统音效失效,如要在所有机型上都能无副作用地展示动效,需要实现自定义Visualizer
。最后对计算出的高度做数据上的平滑处理。首先调用
Consumer
的acquire
方法从BufferQueue
的queued
队列中获取Buffer
,然后从Buffer
中取出所需数据来绘制,最后调用Consumer
的release
方法将上次的Buffer
返回给BufferQueue
。数据处理部分对应
BufferQueue
的Producer
,当 FFT 数据到来时,通过绑定Looper
线程的Handler
将数据发送到Looper
线程中执行数据处理。对于需要绘制多条线或多个点的动效,应该调用drawLines
或drawPoints
方法进行批处理,而不是循环调用drawLine
或drawPoint
方法,以减少执行时间。
参考资料- 傅里叶变换
- 离散傅里叶变换
- 快速傅里叶变换
- 数据平滑
然后根据动效所需数据点数,对频率范围内的 FFT 数据进行采样或用一个 FFT 数据表示多个数据点。
Yi=135(−3yi−2+12yi−1+17yi+12yi+1−3yi+2)Y_i={1 over 35}(-3y_{i-2}+12y_{i-1}+17y_i+12y_{i+1}-3y_{i+2})Yi=351(−3yi−2+12yi−1+17yi+12yi+1−3yi+2)
Yi=121(−2yi−3+3yi−2+6yi−1+7yi+6yi+1+3yi+2−2yi+3)Y_i={1 over 21}(-2y_{i-3}+3y_{i-2}+6y_{i-1}+7y_i+6y_{i+1}+3y_{i+2}-2y_{i+3})Yi=211(−2yi−3+3yi−2+6yi−1+7yi+6yi+1+3yi+2−2yi+3)
Yi=1231(−21yi−4+14yi−3+39yi−2+54yi−1+59yi+54yi+1+39yi+2+14yi+3−21yi+4)Y_i={1 over 231}(-21y_{i-4}+14y_{i-3}+39y_{i-2}+54y_{i-1}+59y_i+54y_{i+1}+39y_{i+2}+14y_{i+3}-21y_{i+4})Yi=2311(−21yi−4+14yi−3+39yi−2+54yi−1+59yi+54yi+1+39yi+2+14yi+3−21yi+4)
经过平滑处理后数据的变化如下图所示:
BufferQueue
有些动效的数据处理计算比较复杂,为提升并行性,减少主线程耗时,我们借鉴系统图形框架中 BufferQueue 的思想,实现了一个简单的承载动效绘制数据,连接数据处理和绘制的 BufferQueue,其工作过程如下图所示:
在使用
BufferQueue
的动效绘制类初始化时,根据需要创建一个合适大小的BufferQueue
,并启动用于执行数据处理的Looper
线程。由于绘制不会用到相位信息,我们可以将用上述方式缩放后的值作为输出 FFT 数据的实部,并把虚部设为 0。这里就需要用到前面提到的模与振幅的计算了,解码所得 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 的计算结果得到最终用来绘制的数据,有必要了解以下DFT特性:- 输入全部为实数时,输出结果满足共轭对称性:XN−k=Xk∗X_{N-k}=X_k^*XN−k=Xk∗,因此一般实现只返回一半结果
- 如原始信号采样率为 fsf_sfs,序列长度为 N,输出频率分辨率为 fs/Nf_s/Nfs/N,第 k 个点的频率为 kfs/Nkf_s/Nkfs/N,可用于查找指定频率范围在结果中对应的位置
- 如一个频率对应输出的实部和虚部为 re 和 im,其模为 M=re2+im2M=\sqrt{re2+im2}M=re2+im2,原始信号振幅为 A={M/NDC2M/NotherA=\begin{cases} M/N & DC \\ 2M/N & other \end{cases}A={M/N2M/NDCother,可用于计算分贝和数据缩放
数据源
提供播放 pcm 数据的 FFT 计算结果的数据源有两种,一种是 Android 系统提供的
Visualizer
类,这种存在兼容性问题,因此我们引入了另一种自己实现的数据源。我们对播放器进行了扩展,增加了收集解码后的 pcm 数据计算 FFT 的功能。平滑
对最终用来绘制的数据做平滑处理,可以得到更柔和的曲线,达到更好的视觉效果,如下图所示:
数据平滑算法有很多,我们综合考虑效果和计算复杂度选择了 Savitzky–Golay 滤波法,其计算方式如下,对应的窗口大小分别为5、
系统
Visualizer
输出的数据大小正比于音量,当音量为 0 时,输出也为 0,可视化效果会随音量变化。自定义 Visualizer
作为跟系统
Visualizer
功能一致的数据源,自定义 Visualizer 需具备两个功能:- 获取 pcm 数据,计算 FFT
- 以指定频率和大小发送 FFT 数据
实现第一个功能首先要获取播放音频的 pcm 数据,这要求使用的播放器能够提供 pcm 数据,我们的播放器是自己实现的,能够满足这个要求。
由于不同音频采样率不同,而计算 FFT 时采用固定的窗口大小,导致 FFT 计算结果回调频率随播放音频改变,同时指定的数据大小可能跟计算结果的大小不同,因此要实现第二个功能,需要对计算结果做固定频率和采样等处理。
- 用 audio session ID 创建