Android 音频可视化:频谱特效的探索与实践

news2024/12/25 12:29:59

音频可视化,一言以蔽之,就是声音到图像的转换。

随着视觉工业时代的到来,用户逐渐重视产品的极致体验,在市场上诸多优秀的音乐类APP中,频谱动效 是一个经典的应用场景:

在这里插入图片描述

图片来源:咪咕音乐

本文以 Android 端为例,从音频信号 数据的获取数据的处理常见问题 几方面进行叙述,针对Android端音频可视化的实现,提供一个通用可行的方案。

一、频谱数据的获取

1.时域与频域

绘制频谱动效,首先要获取歌曲对应的旋律,这里需要先对信号处理中 时域频域 的概念有一个基本的认识:

时域(时间域) 是描述数学函数或物理信号对时间的关系,例如一个信号的时域波形可以表达信号随着时间的变化;
频域(频率域) 是描述信号在频率方面特性时用到的一种坐标系,纵轴是该频率信号的幅度,也就是通常说的频谱图。

对于我们而言,时域虽然是真实存在,但信号的表达 晦涩难懂;而频域相比前者则更加 简洁直观

因此,开发者需进行时域转频域的操作,这里我们借助 傅里叶变换, 将讯号分解成振幅分量和频率分量,将函数的 时域(红色)频域(蓝色) 相关联:

借此我们达到了从频域对模拟信号观察的目的,并将频谱展现给用户,从而提升用户听歌时沉浸式的体验感。

接下来,笔者针对时域转频域中使用到的 FFT算法(非常重要) 进行简单的介绍。

2.FFT-快速傅里叶变换

针对信号分析的最基本方法,称为 离散傅里叶变换(Discrete Fourier Transform,下称DFT) 傅里叶分析方法,它把信号从时域变换到频域,进而研究信号的频谱结构和变化规律。

01_fft_serializer.png

在某些复杂场景下,比如上图对应的有限长序列,使用 DFT 计算量会很大,很难实时地处理问题,因此我们引出了 快速傅里叶变换 (fast Fourier transform, 下称FFT) 算法,其将 DFT 的运算量减少了几个数量级。

引入了这个概念,读者可以理解,为什么总是需要通过FFT算法,拿到对应的数据输出才能完成绘制,为了便于理解,我们将对应输出的 byte[] 称为fft,就和下面Android官方的API声明的一样:

public int getFft(byte[] fft) {
  //...
}

3.原生API的优缺点

FFT算法复杂且易出错,Android 官方也提供了简单的 API —— Visualizer 类便于开发者调用,只需要传入 audioSessionID ,系统就可以自动捕获当前设备在播音频的信号,完成 fft 的转换后,再回调给开发者,开发者直接进行频谱的绘制即可。

简单概括其优势有:

  • 1.系统自动完成音频信号幅度的采样,无需适配,适用于任何原生和三方播放器;
  • 2.系统自动完成 fft 转换,开发者通过回调拿到数据直接绘制,上手成本低。

因此,原生 Visualizer API 理所当然成为了实现方案的首选,但随着实现的深入,更多问题不断暴露出来:

  • 1.音量为0时,回调函数不会返回 fft 数据;
  • 2.类本身只是一个API的壳,真正实现都是在 native 层,难以针对性进行修改和扩展;
  • 3.在某些特殊机型有兼容性问题;
  • 4.由于 Visualizer API 内部是从当前系统获取音频信号,因此使用前 必须授予麦克风权限,否则没有任何返回。

在实际开发开发中,上述缺陷都是产品无法接受的,尤其是 3、4 两条,随着近两年国家推行一系列用户隐私保护的政策,「展示UI特效需要麦克风权限」 是无法说服任何人的,综上所述,原生Visualizer API 方案被果断抛弃。

4.重整思路

现在又回到了原点,我们需要自定义 Visualizer

具体的思路是,在数字化音频时,我们会对信号幅度进行频繁的采样,这称为 脉冲编码调制 (下称PCM)。正如上文对 时域 的描述,振幅随之被量化,但并不直观,因此,我们需要针对 PCM 信号进行结构,即 FFT 算法。

——之前这一切都是交由原生Visualizer API自动完成的,现在由我们自己控制和实现,最终,我们得到了每个时刻声音的频率,并将这些幅度在页面上展示给用户。

5.底层实现

如何获取当前音频的 PCM 数据?这里可以让底层播放器提供,社区比较完善的三方播放器(ijkplayerExoPlayer)等都提供了对应支持,开发者可以根据自身业务调用API或修改源码。

我们不断获得一帧帧的 PCM 数据,通常这是一个 ByteBuffer ,这些 byte 可能来自多个 channel,简化处理可以取所有 channel 的平均值,这之后拿到这些数据进行傅里叶变换。

宏观上理解傅立叶变换,即 N 个时域点变换为 N 个频域点,瓜分采样频率 Fs,频率间隔为△f=Fs / N,根据奈奎斯特采样定理,采样频率是信号最高频率的2倍以上,故有效频点数为N / 2,函数如下:

01_fft_serializer.png

如上文所说,计算机直接进行傅立叶变换复杂度为 O(N^2),一般是采用快速FFT算法,复杂度降低为 O(N*LogN),我们无需手动实现,社区中已有成熟稳定的 三方库 提供这样的支持。

此外,傅立叶变换后的频域点为复数域,不方便做可视化,处理方式是忽略相位信息,仅取(实数)幅度信息。

6.性能优化

即使 FFT 算法相比较 DFT 有一定的优化,但实际执行的性能消耗依然不能忽视,如何针对这一点进行优化?

网易云音乐在 这篇文章 也有提到这个思路:首先,将FFT 算法执行放在播放进程的 native 层中,通过 JNI 调用回调给 java 层后,再将结果数据跨进程传输给主进程,再进行后续的绘制操作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTcOih9w-1689157101651)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b367b8546ec142c7be6253ed65636c9b~tplv-k3u1fbpfcp-watermark.awebp)]

除此之外,由于 PCM 每一帧都包含大量的数据,最初单次的结果 fft 是一个长度为 4096float[]——我们可使用这些频率立即绘制 4096 个条形图,但实际中根本没有这样的场景。

因此,在 FFT 算法执行前,我们可以对不同频段进行初始数据的 二次采样

根据声学,人耳能容易的分辨出100hz200hz的音调不同,但是很难分辨出8100hz8200hz的音调不同,尽管它们各自都是相差100hz,可以说频率和音调之间的变化并不是呈线性关系,而是某种对数的关系。

因此,我们音频的采样以 低频段 为主, 高频段为辅,详细可参考 维基百科。

这样,在 FFT 算法执行时,单次输入的数据源的大小从初始的 4096 降低到了 64128,算法执行速度大幅提升。

二、数据的处理与绘制

拿到 fft 数据后,接下来针对数据各个阶段遇到的问题,分步进行处理。

1.平均算法

首先,因为数据是从不同频段的不同采样点进行获取,因此单个连续数据区间内会存在若干个数据突然很高或很低的情况,这里对数据进行简单的处理,进行 加权平均

01_data_math1.png

2.拟合曲线

对于某一帧的数据而言,即使经过简单的加权平均处理,对文章开头中间的柱状特效而言,展示效果依然不佳,如何将参差不齐的数据经过 平滑处理,形成视觉上的包络效果?

这里我们引入 曲线拟合,顾名思义,就是用连续曲线近似地刻画或比拟平面上离散点组所表示的坐标之间的函数关系的一种数据处理方法。

最小二乘法 是解决曲线拟合问题最常用的方法。其一般形式是:

引入之后,离散化的数据点形成一条光滑的曲线,能明显的提升用户的观感体验。

3.定义衰减系数

经过以上几步处理,单帧效果得到明显改善,但随着划入时间维度之后,相邻两帧的展示效果有明显的跳跃性,对于用户而言就好像掉帧一样。

这里我们引入 衰减系数,每一帧数据绘制前,与上一帧数据进行对比,当单个频率点数据发生较高的抖动,通过衰减系数对抖动进行 抑制

三、其它问题

1.声音和特效不同步问题

这个问题通过gif图是无法体现的,但用户在听音乐时会立即注意到:频谱特效跳跃的太早了,总是比我们实际听到的音乐节奏快了一步。

导致这个现象是因为自定义的播放器在数据传递给 Android 系统的 AudioTrack 之前,我们已经在拿处理好的 fft 进行绘制了,而播放器内部有自定义的缓冲区,这会导致视觉效果领先于音频效果,导致延迟输出。

解决这个问题也非常简单,只需针对PCM数据,参考缓冲区相关源码,再定义一份对应的ByteBuffer即可。

2.绘制方案

对于频谱绘制的技术方案,可以选择系统的 Canvas API 进行自定义View,将数据的相关运算处理在CPU中执行。

考虑到频谱动效本身偏 UI 的展示,且与用户没有直接、复杂的交互,以及性能方面的思量,最终使用了 OpenGL ,并将数据运算相关处理通过矩阵变换,直接交给GPU计算并渲染。

小结

本文针对 Android 端频谱特效实现的整体流程进行概括性的描述,由于频谱动画属于定制化较高的效果,读者无需纠缠于细节,而是将目光聚焦在实际问题的解决思路上。实际开发中,可根据自身产品的需求,灵活运用开发。

参考资料

  • 傅里叶变换-维基百科

  • 快速傅里叶变换-百度百科

  • 最小二乘法-百度百科

  • Android 音频可视化

  • 译|Android Visualizer 可视化器的自定义实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/748202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】C++的IO流

目录 一、C语言的输入与输出 二、流的概念 三、CIO流 1、C标准IO流 2、C文件IO流 四、stringstream的简单介绍 一、C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量…

项目管理:如何减少项目中的信息差

在项目管理中,信息差的存在是难以避免的。 一方面,由于项目干系人各自的角色不同,项目经理不可能让每个人都知道所有的事情,以免在信息的海洋中产生更多的干扰。 另一方面,项目干系人需要了解项目的情况&#xff0c…

华为OD机试真题 Java 实现【数字序列比大小】【2023 B卷 100分】,田忌赛马,永远比你大,你服不服?

目录 一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 大家好,我是哪吒。 做技术,我是认真的,立志于打造最权威的华为OD机试真题专栏,帮助那些与我有同样需求的人&#xff…

ES6——Promise

promise 含义:异步编程解决方案 特点:1、状态不受外界影响,状态有三种:pending、fulfilled、rejected 2、状态不可逆,只能pending -> fulfilled、pending -> rejected 缺点:无法取消、不设置回调函…

常见的人体静电消除器的工作原理

人体静电消除器是一种用于消除人体带有静电的装置。静电是指物体表面具有电荷的现象,当人体带有静电时,会导致一些不舒适的感觉,同时也容易引起电击事故。 人体静电消除器的工作原理主要通过以下几个方面来实现: 1.接地&#xf…

深入篇【Linux】学习必备:认识冯诺依曼系统+理解操作系统(‘‘管理‘‘)

深入篇【Linux】学习必备:认识冯诺依曼系统理解操作系统(管理) Ⅰ.冯诺依曼系统结构1.特点(what)2.理解(why)3.案例(how) Ⅱ.操作系统概念与定位1.概念(what)2.设计OS目的(why)3.管理(how) Ⅰ.冯诺依曼系统结构 1.特点(what) 我们认识的计算机&#xff…

ETHERCAT转ETHERCAT网关三菱plc支持ethercat吗

大家好,今天要和大家分享一款神器——远创智控YC-ECAT-ECAT通讯网关!这款网关有什么厉害的呢?且听我慢慢道来。 首先,YC-ECAT-ECAT是一款自主研发的ETHERCAT从站功能的通讯网关。那什么是ETHERCAT呢?简单来说&#xff…

揭秘ChatGPT的流式返回

107. 揭秘ChatGPT的流式返回 ChatGPT是一种强大的语言模型,可以生成自然语言响应。在传统的请求/响应模型中,客户端发送请求,服务器处理请求后返回响应。但是,使用流式返回可以实现持续的数据流,使得客户端能够实时接…

水文水动力模型在城市内涝、城市排水、海绵城市规划设计中深度应用

随着计算机的广泛应用和各类模型软件的发展,将排水系统模型作为城市洪灾评价与防治的技术手段已经成为防洪防灾的重要技术途径。将聚焦于综合利用GIS及CAD等工具高效地进行大规模城市排水系统水力模型的建立,利用SWMM实现排水系统水力模拟。讲解SWMM深度…

rabbitmq使用springboot实现fanout模式

一、fanout模式 类型&#xff1a;fanout特点&#xff1a;Fanout—发布与订阅模式&#xff0c;是一种广播机制&#xff0c;它是没有路由key的模式。 二、实现 1、引入相应的pom文件 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project x…

左神算法 重要技巧:递归的加速技巧(斐波那契数列套路)以及推广

目录 【案例1】【十分重要 &#xff1a; 斐波那契递归套路&#xff0c;只要像斐波那契这种严格递归都可以进行类似的优化】 【有严格的递归项后&#xff0c;通过线性代数的知识进行优化】 【代码实现】 【技巧推广】 【实例1 使用这个技巧】 【题目描述】 【思路解析】 …

求两个数的最大值max

函数实现 int max(int a, int b); 函数接收两个整数参数&#xff0c;在内部用if语句判断哪个大&#xff0c;返回大的即可。 完整代码 #include <iostream> using namespace std;int max(int a, int b) {if (a > b){return a;}else{return b;} }int main() {int n1…

开发工具篇第25讲:阿里云MFA绑定Chrome浏览器Authenticator插件

开发工具篇第25讲&#xff1a;阿里云MFA绑定Chrome浏览器Authenticator插件 本文是开发工具篇第25讲&#xff0c;登录阿里云旗下产品时&#xff0c;需要使用mfa登录&#xff0c;每次如果要用手机看mfa码很麻烦&#xff0c; Chrome浏览器提供了一个快捷的登录方法&#xff0c;可…

软件工程师,学习下JavaScript ES6新特性吧

概述 作为一名软件工程师&#xff0c;不管你是不是前端开发的岗位&#xff0c;工作中或多或少都会用到一点JavaScript。JavaScript是大家所了解的语言名称&#xff0c;但是这个语言名称是Oracle公司注册的商标。JavaScript的正式名称是ECMAScript。1996年11月&#xff0c;JavaS…

Mysql根据积分排名

积分表&#xff1a;t_participant_points 1、带并列 SELECT p.*, CASE WHEN prevRank p.total_points THEN curRank WHEN prevRank : p.total_points THEN curRank : curRank 1 END AS ranking FROM ( SELECT total_points …

LabVIEW-实现波形发生器

一、题目 用两种方法实现一种多类型信号波形发生器&#xff08;至少包括&#xff1a;正弦波、三角波、方波等&#xff09;&#xff0c;可以调节信号频率、幅度、相位等参数&#xff0c;可以图形化显示信号波形。 需要给出产生信号波形的基本方法、程序设计基本方法以及程序实现…

931.下降路径最小和

931.下降路径最小和 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过 matrix 的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始&#xff0c;并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列&#xff08;即…

【图像处理】经营您的第一个U-Net以进行图像分割

一、说明 AI厨师们&#xff0c;今天您将学习如何准备计算机视觉中最重要的食谱之一&#xff1a;U-Net。本文将叙述&#xff1a;1语义与实例分割&#xff0c;2 图像分割中还使用了其他损失&#xff0c;例如Jaccard损失&#xff0c;焦点损失&#xff1b;3 如果2D图像分割对您来说…

刘铁猛C#教程笔记——详解类型,变量和对象第二节

C#类型派生谱系 C#语言五大基本类型&#xff1a; 1.类类型&#xff1a;由class关键字声明的类型&#xff0c;常见的类类型有&#xff1a;string&#xff0c;object 2.结构体类型&#xff1a;由struct关键字声明的类型&#xff0c;常见的结构体类型有&#xff1a;int&#xff0c…

Vue3 基本语法

尤雨溪 文章目录 前言MVVM框架认识Vue文本渲染指令 v-text属性绑定指令title 属性动态绑定class属性动态绑定style 属性动态绑定 事件绑定 事件名条件渲染指令v-ifv-if 和 v-showv-else 和 v-else-if 循环遍历指令 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&am…