基于Puppeteer离线解析音频频率信息

背景

最近在做微信小游戏时,需要实现一个讲音乐图形化的需求,网上很多用Web Audio API就可以轻松实现把音频转换为图形的例子,比如Web Audio API 绘制可视化音乐这个,但微信小游戏环境目前还不支持Web Audio API,所以想到了办法只能是做离线分析音频的频率了。

那离线要怎么做?找个真实的浏览器环境再写段 JavasScript 脚本去采集?不不不,感觉实现起来就很麻烦,但思路基本是正确的,只是我们不需要用真实的浏览器去解析,大 google 开源了一个headless Chrome的工具Puppeteer,这样我们就可以集成到 nodejs 中,写出基于 Nodejs + Puppeteer的离线解析音频频率的工具。

Puppeteer

首先创建一个能提供AudioContext浏览器环境,并打开一个空白页面:

1
2
3
4
const browser = await puppeteer.launch({
ignoreDefaultArgs: ['--mute-audio'], // 默认是静止声音的
});
const page = await browser.newPage();

解析音频频率信息的代码是在浏览器中执行的,所以需要用到page.evaluate

1
2
3
4
5
// nodejs环境
await page.evaluate(() => {
// 浏览器环境
const audioContext = new AudioContext();
});

在这里我们的已经成功创建audioContext上下文了,剩下的就是怎样解析音频的事情了。

加载音频

首先作为一个工具来说,我们的音频基本是放在本地的,所以需要用 nodejs 的readFile来读取本地文件,这里又抛出了另外一个问题,在 Nodejs 环境下,怎么样把读取的音频文件给到浏览器环境呢?别担心,Puppeteer 已经提供了exposeFunction来处理这种特殊的交互了,首先我们在 nodejs 环境下,定义一个用来加载音频文件函数loadFile,的,注意文件格式选择base64文本格式,buffer格式是无法传递给浏览器环境的:

1
2
3
4
5
6
7
await page.exposeFunction('loadFile', (filePath) => {
return new Promise((resolve, reject) => {
fs.readFile(path.resolve(filePath), 'base64', (err, data) => {
err ? reject(err) : resolve(data);
});
});
});

然后我们在浏览器新建一个函数decode,用来解码 nodejs 传递过来的音频文本信息,最后返回一个AudioBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const decode = () => {
// base64转buffer
const base64ToBuffer = (base64) => {
var binaryString = atob(base64);
var len = binaryString.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(
base64ToBuffer(audioFile),
(buffer) => {
resolve(buffer);
},
(err) => {
reject(err);
},
);
});
};

然后再创建一个音频bufferSource,并把解码后的AudioBuffer添加到 bufferSource:

1
2
3
4
const audioFile = await loadFile('./audio/bgm.mp3');
const audioBuffer = await decode(audioFile);
const bufferSource = audioContext.createBufferSource();
bufferSource.buffer = audioBuffer;

采集音频频率数据

首先我们需要创建一个analyserNode,用来处理获取音频频率数据:

1
2
3
4
const analyserNode = audioContext.createAnalyser();
bufferSource.connect(analyserNode);
analyserNode.connect(audioContext.destination);
bufferSource.start();

然后跑一下代码,这时候已经可以正常听到 bgm 响起了。等等,好像有点不对,难道我要等一首 bgm 全部播完才算采集完吗?没错,AudioContext确实只能是实时采集音频数据。但是好在Web Audio提供了OfflineContext,我们可以在设备不播放音乐的时候,同样采集到数据。我们只有稍微修改一下代码,首先创建一个offlineContext上下文:

1
2
3
4
5
const offlineContext = new OfflineAudioContext(
audioBuffer.numberOfChannels, // 声道数
audioBuffer.length, // buffer大小
audioBuffer.sampleRate, // 采样率
);

然后新增一个scriptProcessorNode,用来实时分析音频信息:

1
const scriptProcessorNode = offlineContext.createScriptProcessor();

最后我们的采集流程是这样的:

也就是 analyserNode 与输出音频直接新增了一个scriptProcessorNode模块,同时该模块注册一个回调方法去采集过程中的音频数据:

1
2
3
4
5
6
7
8
9
10
const analyseData = {};
scriptProcessorNode.onaudioprocess = () => {
const currentTime = Math.floor(offlineContext.currentTime * 100);
if (currentTime % 5 === 0) {
// 采集16位
const dataArray = new Uint8Array(16);
analyserNode.getByteFrequencyData(dataArray);
analyseData[(currentTime / 100).toFixed(2)] = Array.from(dataArray);
}
};

然后再注册一个oncomplete方法,把采集到的数据通过 exposeFunction 暴露到浏览器环境的方法writeFile输出到 json 文件。

最后再播放并渲染音频:

1
2
bufferSource.start();
offlineContext.startRendering();

最后

很多程序语言都能解析音频的频率信息,包括纯 nodejs、python 都可以,本文只是用 Puppeteer 这个无界面浏览器去做音频解析,最后我把以上的处理方案封装成一个命令工具:audio-analyser-cli,感兴趣可以去我的 github 了解:https://github.com/inarol/audio-analyser-cli

[本文谢绝转载,谢谢]

粤ICP备2022084378号