xinglie / xinglie.github.io

blog
https://xinglie.github.io
153 stars 22 forks source link

基于Web音频API的流式音频数据实时分析 #49

Open xinglie opened 5 years ago

xinglie commented 5 years ago

Web音频API是一个高级的JavaScript API,用于处理和合成Web应用程序中的音频。API的目标是实现游戏中的动态声音效果、音乐制作应用程序中的声音处理以及音乐可视化器中的实时分析等功能。

音乐可视化器创建和渲染与音乐属性(频率、响度等)变化同步的动画。大多数媒体播放器(如Windows Media PlayeriTunes)都具有音乐可视化功能

image

在浏览器中创建这种可视化只有通过预先处理音频并单独存储信息来实现,以便在播放期间由可视化器访问。

API目前是一个工作草案,因此可以随时更改。Chrome部分支持(我们使用webkit前缀),我们可以开始尝试、研究它的特性。在本文中,我们将开始研究API的实时分析功能。

音频路由图

API基于音频路由图的概念。最简单的是,音频路由图:由直接连接到声音目标(如计算机扬声器)的单个声源(如MP3文件中的音频数据)组成。

image

通常,路由可以包含连接在一个或多个音源之间并最终连接到目的地(您能听到的)的任意数量的“节点”。音频数据被传入每个节点,以某种方式操作并输出到下一个连接。

使用API主要是为了创建不同类型的节点(一些用于控制音频的各种属性,一些用于添加效果等),以及定义节点应如何连接在一起。正如您所能想象的,与上面显示的简单连接相比,这可以允许更复杂和强大的路由

AudioContext

AudioContext对象是用于创建声源、创建音频操作节点以及定义它们之间的连接的主要抽象。

let context = new webkitAudioContext();

那么,让我们看看如何使用它来创建我们前面展示的简单的从源到目标路由。

首先,我们需要声源。创建音源的一种方法是使用XMLHttpRequestMP3文件中的音频加载到内存中。在下面的代码中,我们使用了AudioContextcreateBufferSource来创建源节点。然后我们使用上下文的createBuffer函数将来自请求的arrayBuffer响应转换为audioBuffer,并使用该函数设置源的buffer属性

let request = new XMLHttpRequest();
request.open("GET", urlToMp3File, true);
request.responseType = "arraybuffer";

request.onload = function() {
    let source = context.createBufferSource();
    source.buffer = context.createBuffer(request.response, false);
}

request.send();

我们不需要创建目标节点。AudioContext有一个destination属性,它表示音频硬件的最终目的地。我们只是通过将源对象连接到AudioContext的目的地来创建路由。

source.connect(context.destination);

流式音源

上面描述的缓冲区方法对于短的音频剪辑是很好的,但是对于较长的声音,我们不希望等待将完整的数据加载到内存中!然而,在音频路由图中,很容易将流式音频输入作为声源。为此,我们使用一个<audio>html元素

<audio id="player" src="urlToMp3"></audio>

<audio>元素表示音频流。AudioContext有一个函数createMediaElementSource,它创建一个声音源节点,该节点将重新路由元素的音频播放并通过路由图传输它

let audioElement = document.getElementById("player");
let source = context.createMediaElementSource(audioElement);
source.connect(context.destination);

您可能需要注意的一个“问题”是,源及其连接可能需要在音频元素准备好播放后创建

audioElement.addEventListener("canplay", function() {
    let source = context.createMediaElementSource(audioElement);
    source.connect(context.destination);
});

分析节点

所以,现在我们的流输入进入路由图,直接进入音频硬件。但是,为了让我们的音乐可视化,我们如何进行实时分析呢?嗯,我说过路线很简单

image

API提供了一个节点,它为我们提供了全部功能——AnalyserNode。我们所需要做的就是创建一个分析节点,并将其连接到源和目标之间的路由图中。在路由图中使用AnalyserNode时,音频数据是通过未经处理的方式从输入传递到输出的,但是我们可以使用node对象实时访问频域和时域分析数据。

如您所料,可以使用AudioContext对象上的createAnalyster函数创建AnalyserNode

let analyser = context.createAnalyser();

为了创建路由图,我们只需在流音频源和目标之间插入分析器。

audioElement.addEventListener("canplay", function() {
    let source = context.createMediaElementSource(audioElement);
    //链接音频源到分析器
    source.connect(analyser);
 //再把分析器链接到目标节点
    analyser.connect(context.destination);
});

默认情况下,分析仪将为我们提供1024个数据点的频率数据。我们可以通过设置fftSize属性来更改此设置。fftSize必须设置为2n次幂,频率分析中的数据点数量始终为fftSize/2。分析器的frequencyBinCount属性将告诉我们在频率数据中要得到的数据点的数量。

console.log(analyser.fftSize); // 2048 by default
console.log(analyser.frequencyBinCount); // will give us 1024 data points

analyser.fftSize = 64;
console.log(analyser.frequencyBinCount); // fftSize/2 = 32 data points

因此,如果我们使用frequencyBinCount元素保留一个字节数组,我们可以随时用频率数据填充它,方法是将它传递给分析器的getByteFrequencyData函数

let frequencyData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);

创建动画

使用此实时数据创建和呈现动画的最佳方法是在requestAnimationFrame回调中刷新频率数据,然后使用新数据更新动画。requestAnimationFrame只是为动画帧安排在下一个适当的时间再次调用函数。它允许浏览器将动画更新与屏幕的重新绘制同步(并可能根据CPU负载进行其他优化,无论页面当前是在后台还是前台选项卡中,等等)。

function update() {
    // 等待下一次绘制
    requestAnimationFrame(update);

    //获取新数据
    analyser.getByteFrequencyData(frequencyData);

    // 更新界面
    bars.each(function (index, bar) {
        bar.style.height = frequencyData[index] + 'px';
    });
};

// 开始绘制
update();

这里,我们只是使用频率数据来设置一些彩色“条”的高度。当然,仅仅在条形图中显示频率数据是最简单的音乐视觉化,但是如果有更多的想象力和创造力,应该可以使用这种方法来创建一些更有趣的音乐视觉化