2. 前端音频处理基础
各位同学,欢迎来到第二章。这一章我们不讲算法,先打好地基。音频降噪说到底,就是在处理一串数字。这串数字怎么来的?浏览器怎么播放它?搞明白这些,后面的滤波器、频谱分析你才能用得顺手。
我个人习惯,学一个新 API 之前,先画一张图,把整个链路看清楚。咱们先看这张音频数字化与 Web Audio 的核心流程:
1. 音频数字化三要素
声音本质是空气振动,在麦克风里变成连续的电压信号。计算机没法处理连续的东西,得把它变成离散的数字。这个过程,就是数字化。
三个核心参数决定了数字音频的质量:
| 参数 | 含义 | 常见值 | 对降噪的影响 |
|---|---|---|---|
| 采样率 | 每秒采集多少个样本点 | 44100 Hz / 48000 Hz | 决定了可处理的最高频率(奈奎斯特频率 = 采样率/2) |
| 位深 | 每个样本用多少 bit 表示 | 16 bit / 24 bit / 32 bit float | 决定了动态范围和信噪比 |
| 声道数 | 独立音频通道的数量 | 1(单声道)/ 2(立体声) | 降噪算法通常先转单声道处理 |
奈奎斯特采样定理:采样率必须大于信号最高频率的两倍,否则会产生混叠失真。人耳可听范围约 20Hz–20kHz,所以 CD 音质用 44.1kHz 采样率,刚好覆盖。
我在项目中遇到过一个问题:用 8kHz 采样率的电话语音做降噪,算法里用了 4kHz 的低通滤波,结果高频噪声根本滤不掉。后来才意识到,8kHz 采样率能表示的最高频率只有 4kHz,噪声和语音混在一起,没法分离。所以做降噪前,先搞清楚你的音频采样率。
2. 创建 AudioContext
Web Audio API 的一切,都从 AudioContext 开始。你可以把它理解成一个音频运行时环境,所有节点都挂在这个上下文里。
// 创建 AudioContext
const audioCtx = new AudioContext();
// 检查采样率
console.log(audioCtx.sampleRate); // 默认通常是 48000
// 恢复音频上下文(浏览器自动挂起策略)
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
浏览器自动挂起策略:Chrome 等浏览器要求用户交互后才能激活 AudioContext。如果你在页面加载时直接创建并播放音频,浏览器会挂起上下文。解决办法是在用户点击按钮时调用 audioCtx.resume()。
嗯,这里要注意:AudioContext 是有状态的。刚创建时是 suspended,调用 resume() 后变成 running,关闭后变成 closed。我建议你在创建后立即检查状态,否则后面节点连好了,音频出不来,排查半天发现是上下文没激活。
3. 音频源节点与目标节点
音频链路说白了就是三样东西:源 → 处理 → 目标。
3.1 音频源节点
源节点是音频数据的起点。Web Audio API 提供了几种常见的源:
- OscillatorNode:生成正弦波、方波等基础波形,适合测试和合成音效
- AudioBufferSourceNode:播放内存中的 PCM 数据,适合加载音频文件
- MediaStreamAudioSourceNode:从麦克风或摄像头获取实时音频流
- MediaElementAudioSourceNode:从
<audio>或<video>标签获取音频
// 创建振荡器源
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine'; // 正弦波
oscillator.frequency.value = 440; // A4 音
// 创建音频缓冲区源
const source = audioCtx.createBufferSource();
source.buffer = audioBuffer; // 预先加载的 PCM 数据
// 从麦克风获取音频流
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const micSource = audioCtx.createMediaStreamSource(stream);
3.2 目标节点
目标节点是音频链路的终点,通常就是扬声器。在 Web Audio API 里,它叫 destination。
// 获取目标节点
const destination = audioCtx.destination;
说白了,你所有处理完的音频数据,最终都要连到 destination 才能被用户听到。如果你做降噪处理,处理完的音频不连到 destination,用户什么都听不到。
4. 基本音频链路搭建
现在我们把源、处理、目标串起来。最简单的链路:振荡器 → 音量控制 → 扬声器。
// 1. 创建上下文
const audioCtx = new AudioContext();
// 2. 创建源节点
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = 440;
// 3. 创建处理节点(音量控制)
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5; // 音量 50%
// 4. 连接:源 → 处理 → 目标
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
// 5. 启动
oscillator.start();
这段代码跑起来,你就能听到 440Hz 的正弦波。但注意,浏览器挂起策略可能让音频不响,记得加个按钮触发 audioCtx.resume()。
调试小技巧:如果你听不到声音,打开浏览器开发者工具的 Console,输入 audioCtx.state 看看是不是 suspended。我曾经花了一下午排查一个降噪 Demo,最后发现是用户没点击页面,AudioContext 一直挂起。
5. 实战:从麦克风采集音频并播放
降噪算法最常用的场景是实时麦克风处理。我们先搭一个基础链路:麦克风 → 扬声器。
async function setupMicrophone() {
// 创建 AudioContext
const audioCtx = new AudioContext();
// 获取麦克风权限
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 创建麦克风源节点
const micSource = audioCtx.createMediaStreamSource(stream);
// 直接连到扬声器(不做任何处理)
micSource.connect(audioCtx.destination);
console.log('麦克风已连接,采样率:', audioCtx.sampleRate);
}
// 用户点击按钮时调用
document.getElementById('startBtn').addEventListener('click', setupMicrophone);
这段代码跑起来,你对着麦克风说话,扬声器会实时播放你的声音。这就是最简单的音频链路——源直接到目标。
但注意,直接这样连会有回声问题。你对着麦克风说话,声音从扬声器出来又被麦克风收进去,形成正反馈啸叫。所以做实时降噪时,通常会用耳机监听,或者加回声消除模块。
6. 音频缓冲区与 PCM 数据
降噪算法处理的是 PCM 数据。PCM 就是脉冲编码调制,说白了就是一系列数字,每个数字代表某个时刻的声压值。
// 创建一个 1 秒的 440Hz 正弦波 PCM 数据
function generateSineWave(audioCtx, frequency, duration) {
const sampleRate = audioCtx.sampleRate;
const length = sampleRate * duration;
const buffer = audioCtx.createBuffer(1, length, sampleRate); // 单声道
const data = buffer.getChannelData(0); // Float32Array
for (let i = 0; i < length; i++) {
data[i] = Math.sin(2 * Math.PI * frequency * i / sampleRate);
}
return buffer;
}
// 使用
const buffer = generateSineWave(audioCtx, 440, 1);
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start();
这里 getChannelData(0) 返回的是一个 Float32Array,取值范围是 -1 到 1。这就是我们降噪算法要处理的数据。后面章节的频谱分析、噪声门、维纳滤波,都是在这个数组上做运算。
关键概念:AudioBuffer 的 getChannelData() 返回的是 Float32Array,每个元素对应一个采样点的幅值。降噪算法本质上就是修改这个数组的值,让噪声成分变小。
7. 本章小结
这一章我们走通了音频数字化的完整链路:模拟信号 → 采样 → 量化 → PCM 数据。然后在浏览器里用 Web Audio API 搭建了最基本的音频链路:源节点 → 处理节点 → 目标节点。
你想想看,降噪算法不管多复杂,最终都是在操作 Float32Array 里的那串数字。采样率决定了你能处理多高的频率,位深决定了你的信噪比上限。这些基础概念不牢,后面做滤波器设计、频谱分析时很容易踩坑。