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

前言

音频可视化,顾名思义就是将声音以视觉的方式呈现出来。如何将音频信号绘制出来?如何将声音的变化在视觉上清晰的表现出来,让视觉和听觉上的感受一致?这些在 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(2pi {kn over N})-i cdot sin(2pi {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=20log_{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 线程。

数据处理部分对应 BufferQueueProducer,当 FFT 数据到来时,通过绑定 Looper 线程的 Handler 将数据发送到 Looper 线程中执行数据处理。数据处理时,首先调用 Producerdequeue 方法从 BufferQueue 中获取空闲的 Buffer,然后对 FFT 数据进行处理,生成需要的数据向 Buffer 中填充,最后调用 Producerqueue 方法将 Buffer 加入到 BufferQueue 中的 queued 队列中。

绘制部分对应 BufferQueueConsumer,调用 Producerqueue 方法时会触发 ConsumerListeneronBufferAvailable 回调,在回调中通过绑定主线程的 Handler 切换到主线程消费 Buffer。首先调用 Consumeracquire 方法从 BufferQueuequeued 队列中获取 Buffer,然后从 Buffer 中取出所需数据来绘制,最后调用 Consumerrelease 方法将上次的 Buffer 返回给 BufferQueue

绘制

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

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

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

结语

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

参考资料

  • 傅里叶变换
  • 离散傅里叶变换
  • 快速傅里叶变换
  • 数据平滑
  • Savitzky–Golay 滤波
  • Android BufferQueue

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

前端如何查看音频的长度_Android音频可视化相关推荐

  1. android pcm频谱_Android音频可视化

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

  2. web前端-纯前端音频剪辑,vue音频编辑组件

    本文包含内容概述: 整理总结的音频相关资料 音频相关jsapi说明及示例 vue编写的组件及git项目地址 在线预览: http://123.57.178.145:5007/audio_edit/in ...

  3. 【Android RTMP】音频数据采集编码 ( AAC 音频格式解析 | FLV 音频数据标签解析 | AAC 音频数据标签头 | 音频解码配置信息 )

    文章目录 安卓直播推流专栏博客总结 一. AAC 音频格式解析 二. FLV 音频数据标签解析 1. 分析 FLV 格式中的 AAC 音频格式数据 2. AAC 音频特殊配置 3. AAC 音频数据标 ...

  4. ios系统html播放音频播放器,iOS音频播放之AVAudioPlayer,AVPlayer,AVQueuePlayer

    本文以婚语APP为例,来讲解集体使用方法. 在婚语APP中,分别使用了AVAudioPlayer,AVPlayer,AVQueuePlayer来实现音频播放功能,下面以婚语的实际需求分别介绍它们的使用 ...

  5. iOS 9音频应用播放音频之iOS 9音频播放进度

    iOS 9音频应用播放音频之iOS 9音频播放进度 iOS 9音频应用开发播放进度 音频文件在播放后经过了多久以及还有多久才可以播放完毕,想必是用户所关注的问题.为了解决这一问题,在很多的音乐播放器中 ...

  6. 【Android RTMP】音频数据采集编码 ( FAAC 音频编码参数设置 | FAAC 编码器创建 | 获取编码器参数 | 设置 AAC 编码规格 | 设置编码器输入输出参数 )

    文章目录 一. 头文件.成员变量准备 二. 创建 FAAC 编码器 三. 获取并设置 FAAC 编码器参数 四. 设置 FAAC 编码器编码标准 五. 设置 FAAC 编码器 AAC 编码规格 六. ...

  7. html audio播放本地语音文件,HTML5+ - audio音频播放及网络音频文件播放

    1.介绍常用方法 createPlayer()创建音频对象 play: 开始播放音频 pause: 暂停播放音频 resume: 恢复播放音频 stop: 停止播放音频 seekTo: 跳到指定位置播 ...

  8. php能转换音频采样率吗,音频采样频率怎么设置-音频采样率转换软件下载

    如果你是一个音乐爱好者,又或者是你很喜欢听音乐,那么你应该会注意到歌曲的采样率,很多的歌曲的采样率是不一样的,不过大部分来说都是48000Hz的.那么音频采样率是什么?音频采样率跟音质有什么关系吗?又 ...

  9. Java fx 变速播放音乐_QVE音频剪辑如何调整音频播放速度?音乐变速方法说明

    为了学习的需要,我们经常会在网上下载很多音频文件.可是当我们听的时候,却发现音频的播放速度要么太快了,要么就是太慢了,听起来不舒服.当遇到这样问题时,我们可以用QVE音频剪辑调整音频播放速度.考虑到很 ...

最新文章

  1. Windows 系统执行Shell 脚本的方法
  2. Excel Services的配置与使用(图解)
  3. android sha1是签名么,Android获取SHA1和MD5签名
  4. ITK:二进制或两张图片
  5. 安装虚拟机和docker封装
  6. ffmpeg分解视频文件并加密
  7. 校招小白机考入坑之从键盘输入java的各种数据类型
  8. memento模式_Java中的Memento设计模式-示例教程
  9. 一步步编写操作系统 47 elf格式文件分析实验
  10. Python 检测系统时间,k8s版本,redis集群,etcd,mysql,ceph,kafka
  11. SpringBoot入门第一个简单示例
  12. 小苏的Shell编程笔记之六--Shell中的函数
  13. 一对多关联关系映射和设置级联属性
  14. 使用promise封装ajax请求
  15. android 手机屏蔽广告 hosts
  16. JavaScript知识点复习总结
  17. 1.3 OC与OD门(硬件基础系列)
  18. Java实现 藏宝架的宝物(分组DP,7.27阿里面试题)
  19. python中cat,stac,transpose,permute,squeeze区别用法
  20. Why and How zk-SNARK Works 1: Introduction the Medium of a Proof

热门文章

  1. 小熊派:用OpenHarmory3.0点亮LED
  2. 同态加密实现数据隐私计算,能让你的小秘密更加秘密
  3. 云图说 | 分布式缓存服务DCS—站在开源Redis前辈的肩膀上,扬帆起航
  4. 透过现象看本质:Java类动态加载和热替换
  5. 华为云IoT专家直播——构筑智慧路灯实战分享
  6. 【华为云技术分享】#华为云·寻找黑马程序员#海量数据的分页怎么破?
  7. 【Python3网络爬虫开发实战】1.7.3-Appium的安装
  8. child计算机英语作文,范文:The One-Child Family
  9. Android笔记 一Android基础知识 摘自第一行代码Android
  10. 特征工程系列学习(一)简单数字的奇淫技巧