zhengrenzhe's blog   About

前端音频处理的超级无敌终极大法-WebAudio

远古时代,我们可以用Flash播放音频,HTML5时代,我们可以用audio标签来播放音频,这已经是一个极大的进步了,但是显然我们可以做得更好,这就得祭出前端音频处理的终极大法-WebAudioAPI

WebAduio(以下简称api)是前端处理音频的最高技术形态,其他任何音频播放技术跟它一比简直就弱爆了。api主要有以下几种功能:

而与其他技术相结合,则更有更多好玩的地方:

开发出更多的功能除了你有丰富的想象力与技术外,最重要的一点是:你必须懂信号处理,傅立叶变换,等大部分通信专业专业课,这样你才能把api玩的转,不然很多功能就算看中文也根本不懂是什么意思=。=

API结构

API的设计采用了路由结构(mdn解释),你也可以把它理解为类似linux的管道(我的解释),也就是一个节点的输入可以当作下一个节点的输出。比如你先应用了高通滤波器,滤去低频,接着连接一个分析节点,此时分析的就是滤去低频的音频,接着再连接一个增益节点,将音频放大,接着再将音频输出。总之,这样的设计对于音频处理是非常方便的。

在firefox中可以直接看到代码中创建的各种节点,上图中一连串的biquadfilter就是我写的一个简易的均衡器,可以看到每一个输出都连接着一个输入,这就是整个API使用的核心原则。

下面是一张API使用基本步骤图:

这张图可以基本说明API的使用方法,图中所涉及的功能只是API中的基础部分,只要你使用API那么基本都会用到。

首先你需要创建一个音频上下文环境,API中的所有操作都是在这个环境中进行的。接着就是音源,大部分情况下音源来资源通过各种渠道加载的二进制数据,所以先经过decodeAudioData()方法解码二进制音频资源,接着将解码过后生成的arraybuffer对象赋值给sourceNode的buffer属性,这样音源就搞定了。现在有了音源,要想实时分析一下音频播放时的数据,可以将sourceNode连接到AnalyserNode这样就可以获得音频播放时的实时频域与时域数据。接着我还想调节一下音量,所以将AnalyserNode连接到GainNode,这样就可以调节音量了。接着就是输出音频了,需要将音频通过Mac的喇叭播放出来,这时将GainNode连接到音频上下文的destinationNode上,这样就完成了一整套音频分析,播放的流程。但此时音频并不会播放,他还是内存中的arraybuffer,这时你要调用sourceNodestart方法,这样音乐就会播放了。

上面先概览一下API的基本使用方法,下面开始进入API的详细使用阶段。

音频上下文

首先是创建音频上下文环境,这有点类似于canvas在使用前要先创建一个绘图上下文,API中所有的方法都会在音频上下文环境中进行。

var ac = new AudioContext();

通过上面一行代码,我们就创建了一个音频上下文环境,接着就进入下一个使用步骤:添加音源。

音源

对于音频处理来说,基础是音源,因为所有的处理都是建立在音源上的。API可以接受四种不同的音频来源,相当丰富,涵盖了基本所有的使用场景:

你可以通过上面任意一种方法来创建音源,下面以最常用的从二进制数据中读取和通过webrtc读取来演示。

var reader = new FileReader(),
    source = ac.createBufferSource();
reader.readAsArrayBuffer(file);
reader.onload = function(){
    ac.decodeAudioData(reader.result, function(decodedData){
	source.buffer = decodedData;
    });
}

这样我们就通过filereader API获取了本地的音频资源,通过ajax获取网络音频资源与上面类似,只是要把ajax的responseType设为arraybuffer ,这样就能获取到网络音频资源的arraybuffer形式了。

navigator.webkitGetUserMedia({video: false,audio: true}, function(stream){
    var source = ac.createMediaStreamSource(stream);
});

通过webrtc,我们可以访问设备的麦克风与摄像头,而API也为这个提供了支持,就是直接通过音频流创建音源。通过音频流创建音源相对比较方便,一行代码就够,但这时的音源与上面通过arraybuffer创建的音源有一点区别,因为一旦用户允许浏览器访问摄像头与麦克风后,音频流是不断产生的,不存在开始或暂停的状态,所以它没有start/stop方法。另外通过webrtc产生的音源也没有buffer属性。除此之外基本没有太大差别。

在运行完上面的代码后,两个变量source就是最上面图中的AudioBufferSourceNode,两者只是有略微区别而已,现在就可以进入到处理音频这一步了。

处理音频

对音频的处理是API的核心功能,这里可能会涉及到专业的信号处理方面的知识,这方面我有一些欠缺,所以我会把我能理解的所有功能在下面挨个写下示例代码,每一段示例代码都可能与前面有一些上下文的联系。

播放音频

大多数情况下,你总是需要播放加载到的音频,除非你要只要进行音频数据分析什么的。通常情况下,你的电脑是有扬声设备的,比如笔记本自带的喇叭。在创建音频上下文后,上下文的destination属性就会自动指向扬声设备,此时你只需要把音源连接到音频上下文的destination属性上,如果是通过二进制数据加载的音频,在调用音源的start方法后,音频就会播放,而通过webrtc加载的音频则会在用户同意读取麦克风后自动播放。

source.connect(ac.destination);
source.start(); // 通过二进制加载音频数据文件在调用start后开始播放,通过webrtc加载的音频则会在用户同意读取麦克风后自动播放。

改变音量

改变音量是最简单也是最基础的功能,大部分情况下你都会需要控制播放时的音量,音量是通过GainNode来控制的,在创建GainNode后,你需要将输入连接到该节点,再将该节点连接到下一个节点。

var gain = ac.createGain();
source.connect(gain);
gain.connect(ac.destination);
gain.gain.value = 0.8;

通过改变value的值,你可以调整音量大小,value的值在0-1之间,0为静音,虽然value也能接受大于1的值,但越大的值破音现象越严重,综上0-1之间比较和谐。

滤波器及其应用

API的强大之处之一就是有丰富的滤波器类型供你挑选,通过组合不同的滤波器,你能对音频进行信号级别的操作,让音频出现不同的效果,而且这一切都是比较傻瓜的,唯一需要的就是你有一些的滤波器基本知识,起码知道滤波器是干啥的。

滤波器,顾名思义就是滤波的,它可以按一定规则滤去音频中的特定部分的波,比如低通滤波器就会让低频信号通过而阻断高频信号,高通滤波器则正好相反。在API中提供了丰富的滤波器,有下面这些类型:

类型 描述
lowpass 低通滤波器,允许低与截止频率的信号通过,减弱高于截止频率的信号信号。
highpass 高通滤波器,允许高于截止频率的信号通过,减弱低与截止频率的信号。
bandpass 带通滤波器,可以减弱给定频率范围之外的音频信号,范围内的通过。
lowshelf 低峰滤波器,允许所有信号通过,但可以增强或减弱比目标频率低的信号。
highshelf 高峰滤波器,允许所有信号通过,但可以增强或减弱比目标频率高的信号。
peaking 在目标频率范围内的信号,可以增强或减弱,范围外的不变
notch 带阻滤波器,可以减弱内定频率范围之内的音频信号,范围外的通过。
allpass 全通滤波器,虽然它允许所有频率通过,但是它会改变信号的相位。

有关滤波器的详细信息,可以查看MDN

通过滤波器,你可以操控音频信号,常见的应用就是EQ。EQ是在各种音乐播放器上常见的功能,中文称为均衡器。通常EQ会提供一些预设组合,比如Bass,Rock,Jazz,Classicla,Dance,Pop等,同时也可以由用户创建自己的EQ。EQ的核心原理,就是对音频信号的某一部分增益或衰减,比如 Bass,也就是低音,就是通过对低频进行增益,来突出低频部分的想过。所以通过API提供的滤波器,你可以轻松的创建一个EQ。

那该选择什么滤波器好呢?根据EQ的原理,当然是用peaking了,它可以对目标频率范围增益或衰减。好了,现在开始创建一个滤波器吧。

var ra = ac.createBiquadFilter(); // 创建一个滤波器
ra.type = 'peaking' // 将滤波器类型设为peaking
ra.Q.value = 10 // 控制频带宽度
ra.frequency.value = 64; // 将截止频率设为64,也就是低频部分
ra.gain.value = 30; // 大于0表示你要增益多少dB,小于0表示你要衰减多少dB

现在一个简单的EQ就出来了,但是现在它并没有什么用,还需要将音源连接到EQ上,再将EQ连接到电脑的扬声设备上。

source.connect(ra);
ra.connect(ac.destination);

这样EQ就能工作了,但这样增益/衰减值是固定的,并且截止频率也是固定的,所以效果就是增强或减弱固定的频率,这肯定是不行滴,没有那个音乐播放器是固定增益衰减一个频率的,所以你还需要创建多个滤波器,将他们串联起来,将截止频率设为不同的值,在弄几个input,由用户决定是增益还是衰减,这样才像一个真的EQ,就像下面这样:

这是我写的一个简易的均衡器,效果还凑活 _(:з」∠)_

混响

混响是个什么呢?其实不难理解,就是给音频加一个特效,让他表现的像在其他环境下发生的一样。比如,一段普通的音频,加了混响就好像是在演唱会里一样。常见的混响效果有空旷地带,工厂,现场等等那些效果,还挺有意思的。在API中,特效来自于一段音频,它本身也是一个可播放的音频,只是通过线性卷积讲它和目标音频结合起来而已。

混响所接受的音频只有buffer,所以特效必须通过本地或网络加载。

var convolver = ac.createConvolver();
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = "arraybuffer";
xhr.send();
xhr.onload = function(){
    ac.decodeAudioData(xhr.response, function(decodedData){
        convolver.buffer = decodedData;
        source.connect(convolver);
        convolver.connect(ac.destination);
    });
}

数据分析及音频可视化

从小到大你一定会经常见到音乐频谱,他会随着音乐的播放增高或降低,通过API提供的分析节点,就可以轻易的创建一个音频可视化效果。

分析节点的功能就是实时的对当前播放着的音频进行快速傅立叶变换,产生实时的频域数据,不得不说这个功能简直太强大了。 要分析音频数据,首先你得创建一个分析节点,并将音源连接到分析节点上。另外需要注意,分析节点可不管连接的音源是原始的还是经过其他处理的,只要是可播放的音源,分析节点就会进行分析,所以如果你好要对原始音源进行分析,则应把分析节点最先连接到音源上。

var analyse   = ac.createAnalyser();
source.connect(analyse);
analyse.connect(ac.destination);
analyse.fftSize = 2048;

现在就穿件了一个分析节点,分析节点只是分析数据,不会对音源进行任何改变。接着就要获取实时快速傅立叶变换产生的频域数据了,这时需要依靠js的类型数据来存储数据了。因为实时分析的数据是大量的二进制数据,所以肯定不能拿js中普通的数组来存取数据,这样可能会有性能问题,所以需要专门类型数组来干这活,类型数组就不细说了,详细的文档在MDN上都有。这里我们使用的是类型数组的子类:Uint8Array,他用来存储8位无符号整形数据。

var ctx = $('#ctx').getContext("2d"),
    line = ctx.createLinearGradient(0, 0, 0, 180),
    num = (analyse.fftSize/2),
    width = 500/num;

line.addColorStop(1,   '#FF0000');
line.addColorStop(0.9, '#FF5A00');
line.addColorStop(0.8, '#FF7700');
line.addColorStop(0.7, '#FFB900');
line.addColorStop(0.6, '#E7FF00');
line.addColorStop(0.5, '#88FF00');
line.addColorStop(0.4, '#75FF00');
line.addColorStop(0.3, '#00FF47');
line.addColorStop(0.2, '#00FFAF');
line.addColorStop(0.1, '#00FAFF');
line.addColorStop(0,   '#00C1FF');
ctx.fillStyle = line;

var arr = new Uint8Array(analyse.frequencyBinCount);

function v(){
    analyse.getByteFrequencyData(arr);
    ctx.clearRect(0, 10, 500, 280);
    for(var i = 0; i < num; i++){
        var f = arr[i];
        ctx.fillRect(i*width , 280-f, width, f);
    }
    requestAnimationFrame(v);
}

requestAnimationFrame(v);

上面就是整个可视化方面的代码。 frequencyBinCount是fftSize的一半,因为分析节点每次产生的数据长度就是fftSize的一半,所以需要创建一个相同大小的类型数据,以便有足够空间存储数据。 通过getByteFrequencyData方法,可以获取到当前的频域数据。但每次调用getByteFrequencyData只会获取当时的数据,要想连续不断的获取频域数据,就要不断的调用getByteFrequencyData来获取数据,这时肯定不能用setInterval,因为数据量一大,还要不停的绘图,它根本跟不上节奏啊,表现就是频谱图有点卡,感觉像掉帧似得,而requestAnimationFrame是专为js动画所提供的api,效果自然比setInterval好得多。 在获取实时的频域数据后,类型数组中的每一项就是频谱途中每一条竖线的高度,使用requestAnimationFrame不断获取数据,每次获取后先清楚canvas,之后在讲每一条竖线绘制到canvas上,最终效果就是这样

因为我前面设置的fftSize是2048,所以每次fft之后复制到类型数组中的元素有1024个,也就是有2014个竖线,所以可视化效果比较密集,如果我改成512,图像就会稀疏一些。

现在常用的API功能基本就讲完了,其他比较有很多都需要比较专业的信号处理知识做铺垫的,不过我这方面学的并不咋滴==,所以也就讲不了太多了。不过掌握了上面的知识,写一个小的在线音乐播放器应该还是没啥大问题的,最后献上一个我写的demo吧,是一个mini在线音乐播放器,点我去玩一下(EQ带上耳机比较明显)。

← JS中与箭头有关的一些东西  scrapy爬知乎带验证码登录 →