Web Audio API 并不会取代 <audio> 音频元素,倒不如说它是 <audio> 的补充更好,就好比如 <canvas> 与 <img> 共存的关系。你使用来实现音频的方式取决于你的使用情况。如果你只是想控制一个简单的音轨的播放,<audio> 或许是一个更好更快的选择。如果你想实现更多复杂的音频处理,以及播放,Web Audio API 提供了更多的优势以及控制。
音频导向图
Web Audio API 基本的音频操作是基于多个 audio nodes 音频节点连接起来形成一个音频导向图。
音频节点可以通过各自的输入与输出相连,形成从多个声源开始,经过处理节点,终止于末节点的链式结构(有时你不需要末节点,比如你只是想数字化处理某些音频数据的时候)。
一个简单、典型的网页音频接口的操作流程可以是这样的:
- 创建一个音频环境。
- 在音频环境中,创建声源——例如
<audio>标签。 - 创建处理节点——例如混响,双二阶滤波,声相控制,音频振幅压缩。
- 选择音频的终点——例如系统的扬声器。
- 连接声源和特效,以及特效和终点。
每个输入和输出都可以包括几个声道,声道代表了一个特定的音效通道。各种声道分离结构都可以使用,包括单声道,立体声,四声道,5.1等等。
我们的音箱看起来像这样:
<body>
<audio id="audio"
src="https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3"
crossorigin="anonymous"></audio>
<button id="play">播放</button>
<button id="pause">暂停</button>
<div>
<label>音量控制</label>
<input type="range" id="volume" min="0" max="1" value="0.5" step="0.01">
</div>
<div>
<label>左右扬声器平衡</label>
<input type="range" id="panner" min="-1" max="1" value="0" step="0.01">
</div>
<script>
const playButton = document.getElementById('play')
const pauseButton = document.getElementById('pause')
const audioElement = document.getElementById('audio')
const volumeInput = document.getElementById('volume')
const pannerInput = document.getElementById('panner')
const audioContext = new AudioContext()
const source = audioContext.createMediaElementSource(audioElement)
const gainNode = audioContext.createGain()
const pannerNode = audioContext.createStereoPanner()
gainNode.gain.value = volumeInput.value
pannerNode.pan.value = pannerInput.value
source.connect(gainNode)
gainNode.connect(pannerNode)
pannerNode.connect(audioContext.destination)
playButton.addEventListener('click', async () => {
if (audioContext.state === 'suspended') {
await audioContext.resume()
}
audioElement.play()
})
pauseButton.addEventListener('click', () => {
audioElement.pause()
})
volumeInput.addEventListener('input', () => {
gainNode.gain.value = volumeInput.value
})
pannerInput.addEventListener('input', () => {
pannerNode.pan.value = pannerInput.value
})
</script>
</body>
- createMediaElementSource:将
<audio>标签元素转换为音频源节点。 - createGain:调节音频的音量。
- createStereoPanner:调节音频在立体声场中的左右分布。
其音频节点:
- createOscillator:生成基础音频波形(如正弦波、方波)。
- createBufferSource:播放内存中的音频片段。
- createBiquadFilter:对音频进行频率过滤(如低通、高通)。
- createDelay:为音频添加延迟 / 回声效果。
- createConvolver:模拟不同空间的声学效果(如混响)。
- createAnalyser:获取音频数据用于可视化(如频谱图)。
- destination:音频的最终输出目标(如扬声器)。
音频上下文
为了能通过 Web Audio API 执行任何操作,我们需要创建音频上下文实例。这能让我们访问 API 所有的特性和功能。
const audioContext = new (window.AudioContext || window.webkitAudioContext)({
// 可选,默认由系统决定
sampleRate: 44100,
});
sampleRate
console.log(audioContext.sampleRate); // 44100(只读)
参与 音频播放。
state
console.log(audioContext.state); // 初始多为 "suspended"
| 状态值 | 含义说明 |
|---|---|
suspended | 暂停状态,不会处理音频。浏览器为了节省资源,默认会延迟启动,直到用户与页面交互。 |
running | 运行状态,音频正常流动。通常在用户点击、触摸等交互后调用 audioContext.resume() 进入。 |
closed | 已关闭状态,上下文被永久关闭,无法再使用。 |
currentTime
为整个音频上下文提供唯一时间基准,所有什么时候开始 / 停止 / 变化都必须用这个数字告诉浏览器。
从音频上下文诞生那一刻起就单调递增,不可回拨、不可手动设置。
给所有音频事件做秒表——start()、stop()、setValueAtTime() 等时间参数都要以它为准。
// 开始播放音频
oscillatorNode.start()
// 播放 0.25 停止播放
oscillatorNode.stop(audioContext.currentTime + 0.25)
destination
任何节点想被用户听见,最终都要直接或间接连到 destination,他代表浏览器默认输出设备(扬声器 / 耳机)。
// 创建音频源数据节点
const oscillatorNode = audioContext.createOscillator();
// 将音频数据传入输出设备播放
oscillatorNode.connect(audioContext.destination);
音频数据来源
Javascript 生成声音节点
<body>
<button id="play">播放</button>
<script>
const playButton = document.getElementById('play')
playButton.addEventListener('click', async () => {
const audioContext = new AudioContext()
const oscillatorNode = audioContext.createOscillator();
oscillatorNode.connect(audioContext.destination);
oscillatorNode.type = 'sine';
oscillatorNode.frequency.value = "523.25";
oscillatorNode.start(0);
oscillatorNode.stop(1);
})
</script>
</body>
PCM 原始数据
<body>
<button id="play">播放</button>
<script>
(async () => {
const playBtn = document.getElementById('play')
const audioUrl = `https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3`
async function convertAudioToPcm(audioUrl) {
const response = await fetch(audioUrl);
const arrayBuffer = await response.arrayBuffer();
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const sampleRate = audioBuffer.sampleRate;
const channelData = audioBuffer.getChannelData(0);
const float32Array = new Float32Array(channelData);
return {
arrayBuffer: float32Array,
sampleRate: sampleRate
}
}
playBtn.addEventListener('click', async () => {
const { arrayBuffer, sampleRate } = await convertAudioToPcm(audioUrl)
const audioContext = new AudioContext();
const audioBuffer = audioContext.createBuffer(1, arrayBuffer.length, sampleRate);
audioBuffer.getChannelData(0).set(arrayBuffer);
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
})
})()
</script>
</body>
- 两次出现的
AudioBuffer音频资源对象:audioContext.decodeAudioData(arrayBuffer):对 MP3、WAV 等压缩格式的原始音频文件二进制数据(arrayBuffer)解码后生成。audioContext.createBuffer(1, arrayBuffer.length, sampleRate):手动创建的空AudioBuffer,需手动写入从第一个AudioBuffer提取的Float32Array采样数据。
audio标签生成声音节点
<body>
<button id="play">播放</button>
<audio id="audio" src="https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3" crossorigin="anonymous"></audio>
<script>
const playButton = document.getElementById('play')
const audioElement = document.getElementById('audio')
playButton.addEventListener('click', async () => {
const audioContext = new AudioContext()
const sourceNode = audioContext.createMediaElementSource(audioElement)
sourceNode.connect(audioContext.destination)
audioElement.play()
})
</script>
</body>
调用createMediaElementSource()之后,<audio>标签的出声权被 Web Audio 音频导向图正式接管。
浏览器不再把原始 <audio>标签直接送进扬声器,必须接入的节点链(示例里就是destination)。在执行audioElement.play()时,如果这条链有没有通到 destination ,用户听不见声音,一旦忘了 connect(或中途 disconnect),页面会立刻静音,而 <audio> 标签的进度条照样跑:这就是典型的节点已脱离音频导向图。
WebRTC MediaStream生成声音节点
<body>
<button id="play">播放</button>
<script>
const playBtn = document.getElementById('play')
playBtn.addEventListener('click', async () => {
const audioContext = new AudioContext()
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
})
const source = audioContext.createMediaStreamSource(stream)
source.connect(audioContext.destination)
})
</script>
</body>
播放多个音频源节点数据
音频节点ended 实现递归播放
<body>
<button id="play">播放</button>
<script>
const playButton = document.getElementById('play')
playButton.addEventListener('click', async () => {
const audioContext = new AudioContext()
const noteFreq = {
C4: 261.63,
D4: 293.66,
E4: 329.63,
F4: 349.23,
G4: 392.00,
A4: 440.00,
B4: 493.88,
C5: 523.25,
};
function playNote(index) {
const oscillatorNode = audioContext.createOscillator();
oscillatorNode.type = 'sine';
oscillatorNode.frequency.value = Object.values(noteFreq)[index];
oscillatorNode.connect(audioContext.destination)
oscillatorNode.start()
oscillatorNode.stop(audioContext.currentTime + 0.25)
oscillatorNode.addEventListener('ended', () => {
if (index + 1 < Object.values(noteFreq).length) {
playNote(index + 1)
}
})
}
playNote(0)
})
</script>
</body>
currentTime 设置不同时间播放多个音频
<body>
<button id="play">播放</button>
<script>
const playButton = document.getElementById('play')
playButton.addEventListener('click', async () => {
const audioContext = new AudioContext()
const noteFreq = {
C4: 261.63,
D4: 293.66,
E4: 329.63,
F4: 349.23,
G4: 392.00,
A4: 440.00,
B4: 493.88,
C5: 523.25,
};
const dur = 0.25;
const now = audioContext.currentTime;
Object.keys(noteFreq).forEach((n, i) => {
const oscillatorNode = audioContext.createOscillator();
oscillatorNode.type = 'sine';
oscillatorNode.frequency.value = noteFreq[n];
oscillatorNode.connect(audioContext.destination);
oscillatorNode.start(now + i * dur);
oscillatorNode.stop(now + (i + 1) * dur);
});
})
</script>
</body>