本文作者:熊鋆洋 (网易云音乐大前端团队)

前言

音频可视化,顾名思义就是将声音以视觉的方式呈现出来。如何将音频信号绘制出来?如何将声音的变化在视觉上清晰的表现出来,让视觉和听觉上的感受一致?这些在 Android 上如何实现?本文将针对这些问题做出解答,尽量对 Android 上的音频可视化实现做一个全面的介绍。

傅里叶变换

Android 音频播放的一般流程是: 1. 播放器从本地音频文件或网络加载编码后的音频数据,解码为 pcm 数据写入 AudioTrack 2. AudioTrack 将 pcm 数据写入 FIFO 3. AudioFlinger 中的 MixerThread 通过 AudioMixer 读取 FIFO 中的数据进行混音后写入 HAL 输出设备进行播放

在这个流程中,直接体现音频特征,可用于可视化绘制的是 pcm 数据。但 pcm 表示各采样时间点上音频信号强度,看起来杂乱无章,难以体现听觉感知到的声音变化。pcm 数据仅可用来绘制体现音频信号平均强度变化的可视化动效,其他大部分动效需要使用对 pcm 数据做傅里叶变换后得到的体现各频率点上信号强度变化的频域数据来绘制。

这里简单回顾下傅里叶变换,它将信号从时域转换为频域,一般用于信号频谱分析,确定其成分。转换结果如下图所示:

pcm 数据是时间离散的,需要使用离散傅里叶变换(DFT),它将包含 N 个复数的序列 $\{x_n\}:=x_0, x_1, ..., x_{N-1}$ 转换为另一个复数序列 $\{X_k\}:=X_0, X_1, ..., X_{N-1}$,计算公式为:

X_k=\sum_{n=0}^{N-1}x_n \cdot e^{-i2 \pi {kn \over N}}=\sum_{n=0}^{N-1}x_n \cdot (cos(2\pi {kn \over N})-i \cdot sin(2\pi {kn \over N}))

直接用上面公式计算长度为 N 的序列的 DFT,时间复杂度为 $O(N^2)$,速度较慢,实际应用中,一般会使用快速傅里叶变换(FFT),将时间复杂度降为 $O(Nlog(N))$。

计算公式看起来很复杂,但不懂也不会影响我们实现音频可视化,FFT 的计算可以使用已有的库,不需要自己来实现。但为了从 FFT 的计算结果得到最终用来绘制的数据,有必要了解以下DFT特性: 输入全部为实数时,输出结果满足共轭对称性:$X_{N-k}=X_k^*$,因此一般实现只返回一半结果 如原始信号采样率为 $f_s$,序列长度为 N,输出频率分辨率为 $f_s/N$,第 k 个点的频率为 $kf_s/N$,可用于查找指定频率范围在结果中对应的位置 * 如一个频率对应输出的实部和虚部为 re 和 im,其模为 $M=\sqrt{re^2+im^2}$,原始信号振幅为 $A=\begin{cases} M/N & DC \\ 2M/N & other \end{cases}$,可用于计算分贝和数据缩放

数据源

提供播放 pcm 数据的 FFT 计算结果的数据源有两种,一种是 Android 系统提供的 Visualizer 类,这种存在兼容性问题,因此我们引入了另一种自己实现的数据源。同时,我们实现了在不修改上层各动效的数据处理和绘制逻辑的基础上切换数据源,如下图所示:

Android Visualizer

系统 Visualizer 提供了方便的 api 来获取播放音频的波形或 FFT 数据,一般使用方式是: 1. 用 audio session ID 创建 Visualizer对象,传 0 可获取混音后的可视化数据,传特定播放器或 AudioTrack 所使用的 audio session 的 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 回调时间间隔差值的不同,我们采用两种不同的方式。

当时间间隔差值小于等于回调时间间隔时,每 $t/ \Delta t$ 次回调丢弃一次数据,其中 t 为 FFT 回调时间间隔,$\Delta t$ 为时间间隔差值,如下图所示:

当时间间隔差值大于回调时间间隔时,每 $t1/t$ 次回调发送一次数据,其中 t1 为所需数据发送时间间隔,t 为 FFT 回调时间间隔,如下图所示:

采样就是当外部设置的数据大小小于 FFT 计算结果的数据大小时,对原始 FFT 数据以合适的间隔抽取数据,以满足设置的要求。

为了让自定义 Visualizer 返回数据的取值范围跟系统 Visualizer 一致,从而实现数据源无缝切换,我们需要对 FFT 数据进行缩放。这里就需要用到前面提到的模与振幅的计算了,解码所得 pcm 数据的取值范围为 [-1, 1],所以原始信号振幅取值范围为 [0, 1],即 $2M/N$ 的取值范围为 [0, 1](绘制时不会用到直流分量,这里不考虑);而系统 Visualizer 返回的 FFT 数据是一个 byte 数组,实部和虚部的取值范围为 [-128, 128],模的取值范围为 $[0, 128 \times \sqrt2]$,那么 $2M/N \times 128 \times \sqrt2$ 的取值范围跟系统 Visualizer 输出 FFT 的模的取值范围一致。由于绘制不会用到相位信息,我们可以将用上述方式缩放后的值作为输出 FFT 数据的实部,并把虚部设为 0。

由于数据发送的频率较高,为了避免频繁创建对象导致内存抖动,我们采用对象池来保存数据数组对象,每次从对象池中获取所需大小的数组对象,填充采样数据后加入到队列中等待发送,数据消费完后将数组对象返回到对象池中。

数据处理

不同动效的具体数据处理方式不同,忽略细节上的差异,云音乐现有的动效中,除了宇宙尘埃和孤独星球,其他的处理流程基本一致,如下图所示:

首先根据动效选择的频率范围计算所需的频率数据在 FFT 数组中的索引位置:

f_r=f_s/N, start=\lceil MIN/f_r \rceil, end=\lfloor MAX/f_r \rfloor

其中 $f_s$ 为采样率,N 为 FFT 窗口大小,$f_r$ 为频率分辨率,MIN 为频率范围起始值,MAX 为频率范围结束值。

然后根据动效所需数据点数,对频率范围内的 FFT 数据进行采样或用一个 FFT 数据表示多个数据点。

然后计算分贝:

db=20\log_{10}M

其中 M 为 FFT 数据的模。

然后将分贝转化为高度:

h=db/MAX\_DB \cdot maxHeight

其中 MAX_DB 是预设的分贝最大值,maxHeight 是当前动效要求的最大高度。

最后对计算出的高度做数据上的平滑处理。

平滑

对最终用来绘制的数据做平滑处理,可以得到更柔和的曲线,达到更好的视觉效果,如下图所示:

数据平滑算法有很多,我们综合考虑效果和计算复杂度选择了 Savitzky–Golay 滤波法,其计算方式如下,对应的窗口大小分别为5、7 和 9,可以按需选择不同的窗口大小。

Y_i={1 \over 35}(-3y_{i-2}+12y_{i-1}+17y_i+12y_{i+1}-3y_{i+2})

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})

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})

经过平滑处理后数据的变化如下图所示:

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。

绘制

绘制部分的主要工作是调用系统 Canvas API 将处理后的数据绘制成所需的效果,具体如何使用 API 绘制,随动效的不同而不同,这里不展开介绍。本节将从对绘制来说比较重要的体验和性能方面介绍一些动效绘制的优化经验。

由于 FFT 数据回调的时间间隔大于 16ms,如果只在数据到来时绘制,会产生视觉上的卡顿,为了得到更好的视觉效果,需要在两次回调之间加入过渡帧,以达到渐变的动画效果。实现方式是在两次数据到达的时间间隔内,以上次数据为起点,本次数据为终点,根据当前时间相对于数据到达时间的消逝时间计算当前的高度,不断重复绘制,如下图所示:

性能优化有两大手段:batch 和 cache,在动效绘制时也可以使用这些手段。对于需要绘制多条线或多个点的动效,应该调用 drawLines 或 drawPoints 方法进行批处理,而不是循环调用 drawLine 或 drawPoint 方法,以减少执行时间。

结语

本文介绍了 Android 音频可视化涉及的背景知识和实现过程,并提供了一些问题解决方案和优化思路。本文专注于通用方案,不涉及特定动效的具体实现,希望读者能从中受到些许启发,实现自己的酷炫动效。

参考资料本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe@corp.netease.com!

android pcm频谱_Android音频可视化相关推荐

  1. android pcm频谱_Android音频开发(7):音乐可视化-FFT频谱图

    Android 音频开发 目录 一.演示 image 二.实现 实现流程: 使用MediaPlayer播放传入的音乐,并拿到mediaPlayerId 使用Visualizer类拿到拿到MediaPl ...

  2. android pcm频谱_Android  音乐频谱实现

    最近在做一款音乐播放器,设计人员新设计样图时 加了一个音乐频谱展示界面,如上图所示.这东西在Window的MediaPlayer中很常见,而且有多种效果. 但不知道怎么实现,我在文档的开发者指南里没有 ...

  3. 前端如何查看音频的长度_Android音频可视化

    本文作者:熊鋆洋 (网易云音乐大前端团队) 前言 音频可视化,顾名思义就是将声音以视觉的方式呈现出来.如何将音频信号绘制出来?如何将声音的变化在视觉上清晰的表现出来,让视觉和听觉上的感受一致?这些在 ...

  4. android pcm 音量_Android中实时获取音量分贝值详解

    基础知识 度量声音强度,大家最熟悉的单位就是分贝(decibel,缩写为dB).这是一个无纲量的相对单位,计算公式如下: 分子是测量值的声压,分母是参考值的声压(20微帕,人类所能听到的最小声压).因 ...

  5. 网易云音乐动效Android,Web Audio在音频可视化中的应用(网易云音乐鲸云动效)...

    本文有两个关键词:音频可视化和Web Audio.前者是实践,后者是其背后的技术支持. Web Audio 是很大的知识点,本文会将重点放在如何获取音频数据这块,对于其 API 的更多内容,可以查看 ...

  6. android pcm文件大小_Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件...

    一.AudioRecord API详解 AudioRecord是Android系统提供的用于实现录音的功能类. 要想了解这个类的具体的说明和用法,我们可以去看一下官方的文档: AndioRecord类 ...

  7. android pcm文件大小_Android中的PCM设备

    Android上的应用一般都是通过AudioTrack类来播放音频,通过AudioRecord类来录制音频.AudioTrack类和AudioRecord类是Android Frameworks封装提 ...

  8. 傅里叶变换音频可视化_H5录音音频可视化-实时波形频谱绘制、频率直方图

    这段时间给GitHub Recorder开源库添加了两个新的音频可视化功能,比以前单一的动态波形显示丰富了好多(下图后两行是不是比第一行看起来丰满些):趁热打铁写了一个音频可视化相关扩展测试代码,下面 ...

  9. java音频频谱_H5录音音频可视化-实时波形频谱绘制、频率直方图

    这段时间给GitHub Recorder开源库添加了两个新的音频可视化功能,比以前单一的动态波形显示丰富了好多(下图后两行是不是比第一行看起来丰满些):趁热打铁写了一个音频可视化相关扩展测试代码,下面 ...

最新文章

  1. 合成孔径成像算法与实现_西安光机所光学成像研究取得进展
  2. AOP切入点表达式 ——within表达式 、execution()表达式
  3. VTK:Points之ExtractSurface
  4. scala使用zip合并两个集合为二元组集合
  5. quartz CronExpression表达式
  6. firefox下可恶的value
  7. 浅谈AsyncLocal,我们应该知道的那些事儿
  8. 一个edit的学习笔记
  9. dart与java互调_Dart与Java不同的地方
  10. linux交叉编译+驱动,请教驱动程序交叉编译问题(初学)
  11. 帝国CMS7.2阿里大于云短信插件(UTF-8)
  12. 李沐《动手学深度学习》中文版PDF和视频免费领!!!
  13. arcmap导出地图快捷键_谷歌点坐标导出为excel表格
  14. 无监督学习与有监督学习的本质区别是什么_干货关于无监督学习你了解多少?...
  15. 带你用Python玩转PPT
  16. python 中 enumerate() 函数使用
  17. 凸包算法-流程及代码简述
  18. 对日软件工程师的几种招聘要求
  19. 为魅族助攻,联发科发布Helio P25处理器
  20. (附源码)ssm智慧社区管理系统 毕业设计 101635

热门文章

  1. RecyclerView+CardView
  2. centos6.8安装完成后重启进不去登录界面_如何在Ubuntu18.04上安装Unity桌面环境?
  3. redis代码 数据超时实现
  4. LibSVM学习(三)——LibSVM使用规范
  5. 第一篇:Django基础
  6. 基于数据挖掘的旅游推荐APP(五):景点推荐模块
  7. 给你总结了这些对付幂等性的套路
  8. Istio分层架构?80%的人有误解
  9. 互联网架构“高并发”到底怎么玩?
  10. 在阿里干了五年,面试个小公司挂了…