请原谅使用了这么标题党的标题做标题,本文完全闲着没事时候干的,只是一些有趣的效果,可能对大部分同学的工作上来说没有任何帮助。
使用window.AudioContext
(音频模块)API开发一款可视化音频的效果,因为gif不能录制声音,所以大家凭想象脑补一下吧,背景音乐是《极乐净土》。gitee上会上传文件
注册音频模块
let AudioContext = window.AudioContext;
var audioContext = new AudioContext();//实例化
官网地址中介绍AudioContext
表示由链接在一起的音频模块构建的音频处理图并提供以下几种方法:
AudioContext.close() 关闭一个音频环境,释放任何正在使用系统资源的音频。
AudioContext.resume() 恢复之前被暂停的音频上下文中的时间进程。
AudioContext.suspend() 暂停音频上下文中的时间进程,暂停音频硬件访问并减少进程中的 CPU/电池使用。
并且在官网中也提供了解决跨浏览器的方案var AudioContext = window.AudioContext || window.webkitAudioContext;
加载音频文件
由于请求的是本地文件,所以比较周折,第一步先获取本地文件地址,第二步通过axios
请求到本地文件,第三步骤将请求到的二进制转为Blob
文件,第四步骤使用fileReader
将Blob
加载出来,
请求文件
import audioUrl from '../assets/audio/jljt.mp3'
import axios from 'axios'
···
// 通过axios请求文件
const getBlob = (): Promise<Blob> => {
return new Promise((resolve, reject) => {
axios({
method: 'GET',
// 请求文件必须修改请求类型,否则请求到的二进制无效
responseType: 'blob',
url: audioUrl
}).then((res) => {
// 通过请求的二进制生成blob类型文件
const blob = new Blob([res.data], { type: 'audio/mpeg' })
resolve(blob)
})
})
}
// 请求音频文件
const audioBlob = await getBlob()
// FileReader 接口提供的 readAsArrayBuffer() 方法用于启动读取指定的 Blob 或 File 内容
fileReader.readAsArrayBuffer(audioBlob);
// 文件加载完成后调用
fileReader.onload = fileRenderOnload
当文件加载完毕,会调用fileReader.onload
方法,之后就可以使用audioContext
提供的decodeAudioData
方法解析文件
AudioContext
接口的decodeAudioData()
方法可用于异步解码音频文件中的ArrayBuffer
。ArrayBuffer
数据可以通过XMLHttpRequest
和FileReader
来获取。AudioBuffer 是通过 AudioContext 采样率进行解码的,然后通过回调返回结果。
这就是上面所说的为什么那么复杂的步骤还是需要用FileReader获取文件,人家就这么规定的,咱们条件又不允许肆意妄为。还有一种方案,使用input 上传文件的方式,采用这个方案,感觉代码不是很高级,哈哈哈哈~,但这种方法确实简单,只不过在调试的时候,每次热更新都要上传一次文件,比较麻烦而已。
解析音频文件
在文件加载后,调用fileRenderOnload
方法
// 加载完毕解码
const fileRenderOnload = (e: any) => {
// 异步解码音频文件中的 ArrayBuffer
audioContext.decodeAudioData(e.target.result, function (buffer) {
// 加载完毕
// 创建一个AnalyserNode
analyser = audioContext.createAnalyser();
// 创建一个新AudioBufferSourceNode接口
audioBufferSourceNode = audioContext.createBufferSource();
// 范围[32, 32768]
// AnalyserNode 接口的 fftSize 属性的值是一个无符号长整型的值,表示(信号)样本的窗口大小。
// 当执行快速傅里叶变换(Fast Fourier Transfor (FFT))时,这些(信号)样本被用来获取频域数据。
// fftSize 属性的值必须是从 32 到 32768 范围内的 2 的非零幂; 其默认值为 2048.
analyser.fftSize = 512; // 数值越大,消耗资源越多,越慢
audioBufferSourceNode.buffer = buffer;
})
}
解析后的回调提供一个buffer
参数
这是buff输出的参数,有持续时间,文件大小等信息(好像有什么问题,解析出的时间长度跟文件的时间长度不一样)
有可能是解析能力不足导致的,没深究
值得注意的是fftSize
参数,数值越大采样频率越高,消耗资源越高
AnalyserNode
接口的fftSize
属性的值是一个无符号长整型的值,表示(信号)样本的窗口大小。当执行快速傅里叶变换(Fast Fourier Transfor (FFT))时,这些(信号)样本被用来获取频域数据。
fftSize 属性的值必须是从 32 到 32768 范围内的 2 的非零幂; 其默认值为 2048.
官网是这样介绍的,没有点相关专业知识真的看不懂,大概知道他的值有哪些可以选就可以了。
播放
前文创建的AnalyserNode
,可以获取和处理音频,将解析后的音频buffer
赋值给AnalyserNode
,
AnalyzerNode
只有一个输入和输出,即使未连接到输出它也能正常工作。
工作原理
AnalyzerNode
>AudioContext
>createBufferSource
这几个属性需要一一关联才能够播放出来,audioBufferSourceNode.start();
if (btn) {
btn.onclick = () => {
// 将音频链接到Node,可以听到音频
audioBufferSourceNode.connect(AnalyserNode);
AnalyserNode.connect(audioContext.destination);
// 创建的node开始播放
audioBufferSourceNode.start();
}
}
通过按钮点击将声音播放出来,AnalyserNode
解析到的声音是实时变化的,为了能够具象化这种变化,我们需要在requestAnimationFrame
重绘方法中获取到,并在之后的内容中使用
const draw = () => {
requestAnimationFrame(() => draw());
// 文件信息
var bufferLength = AnalyserNode.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
for (var i = 0; i < bufferLength; i++) {
let barHeight = dataArray[i];
AnalyserNode.getByteFrequencyData(dataArray);
}
}
这里使用的API文档中每一条都很长很长,特别晦涩难懂的,感兴趣的同学可以试一下,大概的意思就是获取到fftSize
值的一半,并通过这个长度创建一个Uint8Array
数组,通过getByteFrequencyData
方法,将解析出来的音频采样采集到dataArray
中,下面是最终获取到的值。
可视化音频
获取到这些不同的值之后,即可操作dom(或者canvas,或者threejs,或者svg等任何你能想到的创意)实现不同的音频可视化,
创建dom
前文定义的fftSize
值是512,解析出来的dataArray
长度是256,那咱们创建128个li并使用less写好样式,这里没啥讲的,都是基本的东西,直接贴代码了,在vscode中执行命令ul>li*128>div>div
创建出dom,li是确定每一个点位的位置,li > div
是可视化的dom,li>div>div
是阴影部分,
@bar-deg: 2.8125deg;
@bar-length: 128;
ul {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
z-index: -1;
// transform: rotate(90deg)
}
li {
position: absolute;
margin: auto;
height: 28px;
}
each(range(@bar-length), {
li:nth-of-type(@{value}) {
transform:rotate((@value * @bar-deg));
transform:rotate((@value * @bar-deg)); // 旋转角度
left: (50vw + sin((2 * pi() / 360) * @bar-deg)); // 圆切点坐标
top: (50vh + cos((2 * pi() / 360) * @bar-deg));
&>div {
width: 2px;
height: 4px;
min-height: 4px;
max-height: 30px;
position: absolute;
left: 0px;
top: 64px;
bottom: 0;
margin: auto;
transform: translateY(100px);
background: if((@value >(@bar-length/2)), #6fe9a6, #6eeff9);
&>div {
position: absolute;
left: 0px;
// top: 64px;
bottom: 0;
margin: auto;
width: 4px;
height: 3px;
max-height: 30px;
// box-shadow: 10px 0px 10px if((@value >(@bar-length/2)), #6fe9a6d1, #6ef0f9b3);
// background-color: rgba(255, 0, 255, 0) // z-index: -1;
}
}
}
}
);
通过less把大致的样式写一下,如下:
阴影写完注释掉了,后续在ts中完善。
完善
...
let barHeight = dataArray[i];
AnalyserNode.getByteFrequencyData(dataArray);
const liDiv = document.querySelector(`li:nth-of-type(${i}) > div`) as any
const liDivDiv = document.querySelector(`li:nth-of-type(${i}) > div > div`) as any
if (liDiv) {
const height: number = Number(`${barHeight / 100 * Math.pow(8, barHeight / 100 - 0.7)}`)
liDiv.style.height = `${height}px`
liDivDiv.style.height = `${height * 0.7}px`
const color = liDivDiv?.getAttribute('shadowColor')
liDivDiv.style.boxShadow = `0px ${Math.min(height * 0.7,30)}px 10px ${color}, 0px ${Math.max(-height * 0.7,-30)}px 10px ${color}`
}
...
barHeight
就是不同采样后的不同频率,根据频率不同,改变li>div
的高度和li>div>div
的阴影尺寸,
Math.pow(8, barHeight / 100 - 0.7)
作为一个系数,使barHeight
值大的变大,小的变小,这样会产生强烈的对比,不然的话,幅度没那么明显,不同的系数可以影响dom的效果,都可以试试不同的系数和阴影方向,或者替换不同的音乐,做出不一样的可视化音频吧。