简介
Web Audio API主要是用来对网页中的音频做处理,如果觉得audio标签不足以支撑自己对声音播放的需求,则可以尝试用Web Audio API来实现,后续文中Web Audio API 都以 API 称呼。
官网对API的解释如下:
Web Audio API 使用户可以在音频上下文(AudioContext)中进行音频操作,具有模块化路由的特点。在音频节点上操作进行基础的音频,它们连接在一起构成音频路由图。即使在单个上下文中也支持多源,尽管这些音频源具有多种不同类型通道布局。这种模块化设计提供了灵活创建动态效果的复合音频的方法。
浏览器支持
API文档
基础概念
音频图
API对音频的处理都是基于音频图来进行的,可以当做是一个音频处理的流程图,其中音频和音频处理模块都是节点,节点有对应的输入或输出,将节点通过输入输出链接起来后,就形成了音频图。
AudioBuffer
AudioBuffer 接口表示驻留在内存中的短音频资产,这些类型的对象旨在保存小音频片段,通常小于 45 秒。该接口创建时有以下几个属性:
- 声道数量(1 用于单声道,2 用于立体声等)
- 长度,即缓冲区内的样本帧数
- 采样率,每秒播放的采样帧数
创建代码如下:
const context = new AudioContext();
const buffer = context.createBuffer(2,22050,44100);
这表示创建一个采样率为44100hz,长度为22050帧的双通道音频,时长为22050/44100即0.5s,每帧有两个通道的数据。
AudioContext
音频api所有的操作,都是基于一个上下文来进行的,不同上下文是不同的音频图。
AudioDestinationNode
音频最终输出地址,一般是系统默认扬声器,而使用时也不需要单独初始化,直接使用audioContext.destination即可。
currentTime
音频播放有时间轴,该时间为硬件时间戳(以秒为单位),而通过上下文获得currentTime返回的是一个当前硬件时间戳的时间。
listener
该接口可以初始化一个处于3d空间下的听者,如果2d层面只有左右声道的话,3d就是全方位的声道,声音可以在3d空间的任意位置,以及旋转。
OfflineAudioContext
与音频上下文作用一致,但区别在于离线音频上下文的目的不在于播放音频,而只是对音频做一些处理。
Audio Workers
用于通过 JavaScript 代码生成,处理,分析音频,但是目前仍是由ScriptProcessorNode节点来进行这一工作,还没有浏览器实现audio workers的api实现。
可视化音频图
因为api是基于音频图来进行音频处理的,那么如果可以可视化音频图,就可以看到各个节点的链接方式与输入输出的类型及其他参数。而这一功能在之前版本的火狐浏览器中被实现,但因为音频图调试器使用人数过少,在新版本的火狐浏览器中已被移除,但是可以通过扩展实现这一调试器。
在谷歌商店中找到audion插件,安装后启用,当页面存在web audio api时,将开始监听,并绘制出音频图可视化图。
API的基础使用
简单案例
一个简单而典型的 web audio 流程如下:
- 创建音频上下文
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
这里blink内核的浏览器和webkit内核的浏览器做了一下兼容,需要注意的是,safari浏览器再windows以外的系统是无法运行的。
- 获取要播放的音频
<audio src="track.mp3" type="audio/mpeg"></audio>
const audioElement = document.querySelector('audio');
const track = audioContext.createMediaElementSource(audioElement);
- 为音频选择一个目的地,连接源到效果器,对目的地进行效果输出
track.connect(audioContext.destination);
到这已经有一个流程了,但是没有对音频进行处理直接输出,因此可以听到audio标签播放的是源文件。
当前音频图如图所示:
可能出现的问题:
- The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page.
这个问题是浏览器的自动播放策略,要求音频上下文必须在用户互动后创建,但如果是在互动前创建的上下文,则需要用一下代码进行启动。
if (audioContext.state === 'suspended') {
audioContext.resume();
}
- play() failed because the user didn’t interact with the document first.
window.addEventListener('click', () => {
audioElement.play();
});
音频源
上一个案例中,音频是从一个加载了mp3文件的audio标签中获取,然后通过音频上下文创建了多媒体标签音频源,除此以外还有其他多种使用音频的方式:
AudioBufferSourceNode
该节点播放的是AudioBuffer,即音乐数据缓存在内存中,AudioBufferSourceNode 只能播放一次;每次调用 start() 后,如果要再次播放相同的声音,则必须创建一个新节点。但是AudioBuffer是数据类型,可以重复使用,所以节点的开销并不大。
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Create an empty three-second stereo buffer at the sample rate of the AudioContext
const myArrayBuffer = audioCtx.createBuffer(
2,
audioCtx.sampleRate * 3,
audioCtx.sampleRate
);
// Fill the buffer with white noise;
//just random values between -1.0 and 1.0
for (let channel = 0; channel < myArrayBuffer.numberOfChannels; channel++) {
// This gives us the actual ArrayBuffer that contains the data
const nowBuffering = myArrayBuffer.getChannelData(channel);
for (let i = 0; i < myArrayBuffer.length; i++) {
// Math.random() is in [0; 1.0]
// audio needs to be in [-1.0; 1.0]
nowBuffering[i] = Math.random() * 2 - 1;
}
}
// Get an AudioBufferSourceNode.
// This is the AudioNode to use when we want to play an AudioBuffer
const source = audioCtx.createBufferSource();
// set the buffer in the AudioBufferSourceNode
source.buffer = myArrayBuffer;
// connect the AudioBufferSourceNode to the
// destination so we can hear the sound
source.connect(audioCtx.destination);
// start the source playing
source.start();
MediaElementAudioSourceNode
MediaElementAudioSourceNode 接口代表着某个由 HTML 或 元素所组成的音频源。该接口作为 AudioNode 音源节点。
MediaStreamAudioSourceNode
MediaStreamAudioSourceNode 接口代表一个音频接口,是WebRTC MediaStream (比如一个摄像头或者麦克风) 的一部分。是个表现为音频源的AudioNode。
OscillatorNode
OscillatorNode 接口表示一个振荡器,它产生一个周期的波形信号(如正弦波)。它是一个 AudioScheduledSourceNode 音频处理模块,这个模块会生成一个指定频率的波形信号(即一个固定的音调)。
音量控制
在目前的流程上,添加一个用于修改音量的音频处理模块**GainNode,**增益是一个无单位的值,会对所有输入声道的音频进行相应的增加(相乘)。
- 初始化一个gainnode
const gainNode = audioContext.createGain();
- 在页面添加一个input滑动调整音量
<div>
<span>音量:</span>
<input type="range" name="volumn" min="0" max="10" value="1" step="0.1" id="volumn"
onchange="changeVolumn(this.value)">
<span id="volumn-text">10</span>%
</div>
const volumnElement = document.getElementById('volumn');
const volumnTextElement = document.getElementById('volumn-text');
function changeVolumn(value) {
gainNode.gain.value = value
volumnTextElement.innerText = value*10;
}
- 将gainNode连接到当前的流程上
track.connect(gainNode).connect(audioContext.destination);
当前音频图如图所示:
声道控制
声道控制也是一个名为StereoPannerNode的音频控制模块来负责处理,使用低成本等功率平移算法将传入的音频流定位在立体声图像中。
- 初始化一个StereoPannerNode
const pannerOptions = { pan: 0 };
const panner = new StereoPannerNode(audioContext, pannerOptions);
- 在页面添加一个input滑动调整声道
<div>
<span>声道:</span>
<span>左</span>
<input type="range" name="stereo" min="-1" max="1" value="0" step="0.1"
onchange="changeStereo(this.value)">
<span>右</span>
</div>
function changeStereo(value) {
panner.pan.value = value;
}
- 将panner连接到当前的流程上
track.connect(gainNode).connect(panner).connect(audioContext.destination);
当前音频图如图所示:
使用振荡器
之前是从节点获取到音源播放,而api也有声音产生模块,即振荡器。它可创建给定波的指定频率,也就是恒定的音调。
- 初始化一个OscillatorNode
const oscillator = audioContext.createOscillator();
oscillator.type = "square";
oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
这里type为square意思为当前振荡器发出的声音为方波,而440是波形的频率,该频率演奏出的声音即音乐意义上的a1,也是国际标准音高。
- 将振荡器连接到音频图然后播放
oscillator.connect(gainNode).connect(panner).connect(audioContext.destination);
oscillator.start();
当前音频图如图所示:
这时候已经可以听到一个振荡器刺耳的声音了,如果需要停下,则调用stop方法。
oscillator.stop();
这里有一个问题需要注意的是,每一个振荡器都是只允许单次播放,即start后再stop视为该振荡器已经使用过了,如果再次start,将会弹出报错:Failed to execute ‘start’ on’AudioScheduledSourceNode’: cannot call start more than once.
因此需要封装一个方法来播放振荡器,每次播放都创建一个新的振荡器实例。
/**
* Initializes an oscillator with the given type, frequency, and duration.
*
* @param {string} type - The type of oscillator to create.
* @param {number} frequency - The frequency of the oscillator.
* @param {number} duringtime - The duration of the oscillator in milliseconds.
*/
function oscillatorPlay(type,frequency,duringtime) {
// 初始化振荡器
const oscillator = audioContext.createOscillator();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
oscillator.connect(gainNode);
oscillator.start(audioContext.currentTime);
setTimeout(() => {
oscillator.stop();
oscillator.disconnect(gainNode)
}, duringtime)
}
附上小字一组频率表:
频率(hz) | 音名 |
---|---|
262 | c1 |
277 | c1♯/d1♭ |
294 | d1 |
311 | d1♯/e1♭ |
330 | e1 |
349 | f1 |
370 | f1♯/g1♭ |
392 | g1 |
415 | g1♯/a1♭ |
440 | a1 |
466 | a1♯/b1♭ |
494 | b1 |
有了滤波和频率,就可以做一些简单的音乐:
根据以上频率和五线谱对照关系,以及乐谱,就可以使用滤波器演奏音乐(此处需要打开音频输出设备):
除此以外,还可以绘制键盘来手动演奏,如mdn示例:
live-samples.mdn.mozilla.net/en-US/docs/…sample.the_video_keyboard.html
音频可视化
api中有AnalyserNode模块可以分析音频数据,获取频率信息,通过这些信息,可以对音频进行可视化。
- 初始化一个AnalyserNode
const analyser = audioCtx.createAnalyser();
- 将音频分析节点连接到音频图
analyser.connect(audioCtx.destination);
- 设定音频数据格式与大小
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
- 获取当前音频数据
analyser.getByteTimeDomainData(dataArray);
拿到数据后,可以使用这些数据来做一些可视化操作,比如在canvas中绘制频率图或波形图。
音频频率图
analyser.fftSize = 256;
let bufferLength = analyser.frequencyBinCount;
let dataArray = new Uint8Array(bufferLength);
let barWidth = (WIDTH / bufferLength) * 2.5;
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
function draw() {
drawVisual = requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
canvasCtx.fillStyle = 'rgb(0, 0, 0)';
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
let barHeight;
let x = 0;
for(var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i]/2;
canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',50,50)';
canvasCtx.fillRect(x,HEIGHT-barHeight/2,barWidth,barHeight);
x += barWidth + 1;
}
};
音频波形图
analyser.fftSize = 2048;
let bufferLength = analyser.fftSize;
let dataArray = new Uint8Array(bufferLength);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
function draw() {
drawVisual = requestAnimationFrame(draw);
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
canvasCtx.beginPath();
let sliceWidth = WIDTH * 1.0 / bufferLength;
let x = 0;
for(var i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * HEIGHT/2;
if(i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height/2);
canvasCtx.stroke();
};
音频空间化
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
const listener = audioCtx.listener;
const posX = window.innerWidth / 2;
const posY = window.innerHeight / 2;
const posZ = 300;
listener.positionX.value = posX;
listener.positionY.value = posY;
listener.positionZ.value = posZ;
listener.forwardX.value = 0;
listener.forwardY.value = 0;
listener.forwardZ.value = -1;
listener.upX.value = 0;
listener.upY.value = 1;
listener.upZ.value = 0;
延迟节点
该节点可以使音乐播放延后一段时间
const audioCtx = new AudioContext();
const synthDelay = audioCtx.createDelay(5.0);
let synthSource;
playSynth.onclick = () => {
synthSource = audioCtx.createBufferSource();
synthSource.buffer = buffers[2];
synthSource.loop = true;
synthSource.start();
synthSource.connect(synthDelay);
synthDelay.connect(destination);
};
使用库来简化代码量
Tone.js
Tone.js是一个用于在浏览器中创建交互式音乐的Web Audio框架。Tone.js的架构旨在对音乐家和音频程序员都很熟悉,以创建基于Web的音频应用程序。在高层次上,Tone提供了常见的DAW(数字音频工作站)功能,例如用于同步和调度事件的全局传输以及预先构建的合成器和效果器。此外,Tone提供了高性能的构建模块,以创建自己的合成器,效果器和复杂控制信号。
安装
npm install tone
播放音频文件
const player = new Tone.Player("track.mp3").toDestination();
player.autostart = true;
音量控制
const gainNode = new Tone.Gain(0).toDestination();
const osc = new Tone.Oscillator(30).connect(gainNode).start();
gainNode.gain.rampTo(1, 0.1);
使用合成器
tonejs中常用的合成器为Synth,该合成器基于oscillator和其包络组成。即在振荡器的基础上,有了包络调整音色,使声音更丰富。
const synth = new Tone.Synth().toDestination();
synth.triggerAttackRelease("C4", "8n");
使用采样器
采样器是指可以将一个音频文件作为采样,在需要时播放这段音频文件。使用Sampler时,可以只加载部分音频文件,其他音调将通过将这些文件升降调实现播放。
const sampler = new Tone.Sampler({
urls: {
A1: "A1.mp3",
A2: "A2.mp3",
},
baseUrl: "https://tonejs.github.io/audio/casio/",
onload: () => {
sampler.triggerAttackRelease(["C1", "E1", "G1", "B1"], 0.5);
}
}).toDestination();