重采样
重采样是音频处理中调整采样率的过程,目的是使音频数据适配不同的播放设备或处理需求。采样率定义为每秒的采样点数,单位为 Hz,直接影响音频的频率响应范围和数据量。
AudioContext.decodeAudioData 的自动重采样
decodeAudioData 是 Web Audio API 中用于解码编码音频格式(MP3、WAV 等)的心方法,其核心特性之一是:会根据 AudioContext 的基准采样率自动重采样,确保生成的 AudioBuffer 采样率与基准值一致,避免播放时出现快放 / 慢放。
<body>
<button id="playBtn">播放</button>
<script>
(async () => {
const playButton = document.getElementById('playBtn')
playButton.addEventListener('click', async () => {
// 1. 拉取远程 MP3 音频(假设原始采样率为 44100Hz)
const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
const response = await fetch(audioUrl)
const arrayBuffer = await response.arrayBuffer()
// 2. 创建音频上下文,固定基准采样率为 16000Hz
const audioContext = new AudioContext({
sampleRate: 16000 // 基准采样率 = 目标播放采样率
})
// 3. 自动重采样:无论原始音频采样率是多少,解码后 audioBuffer 采样率 = 16000Hz
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
console.log('自动重采样后采样率:', audioBuffer.sampleRate) // 输出 16000
// 4. 播放:audioBuffer 采样率与硬件播放采样率一致,正常语速播放
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(audioContext.destination)
source.start()
})
})()
</script>
</body>
- 触发条件:当原始音频(如示例中 MP3)的采样率 ≠
AudioContext.sampleRate时,decodeAudioData自动执行重采样。 - 最终结果:
audioBuffer.sampleRate强制对齐AudioContext.sampleRate,确保后续播放时无速度 / 音调异常。 - 局限性:仅支持解码 编码音频格式(MP3、WAV、AAC 等),无法直接处理 裸 PCM 二进制数据(无格式头的纯采样数据)。
PCM 转 WAV:让 decodeAudioData 支持裸 PCM
裸 PCM 数据仅包含采样点二进制信息,无采样率、位深等元数据,decodeAudioData 无法解析。需先将其封装为 WAV 格式(补充格式头),再进行解码。
<body>
<button id="playBtn">播放</button>
<script>
(async () => {
const playButton = document.getElementById('playBtn')
// 步骤1:从 MP3 解码获取裸 PCM 数据(模拟“仅拥有裸 PCM”场景)
async function generatePcm() {
const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
const response = await fetch(audioUrl)
const arrayBuffer = await response.arrayBuffer()
// 临时音频上下文:解码 MP3 得到 44100Hz 的 PCM(Float32 类型,单声道)
const tempAudioContext = new AudioContext({ sampleRate: 44100 })
const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer)
return audioBuffer.getChannelData(0) // 返回裸 PCM 数据
}
// 步骤2:将裸 PCM 封装为 WAV 格式(补充 WAV 头信息)
function pcmToWav(pcm32Buffer, sampleRate) {
const bitsPerSample = 16; // 16位深度(PCM 存储格式)
const numChannels = 1; // 单声道
const pcm16Buffer = new Int16Array(pcm32Buffer.length);
// 1. Float32 类型 PCM 转为 16 位整数(WAV 常用格式)
for (let i = 0; i < pcm32Buffer.length; i++) {
// 限制范围:16位整数的取值范围是 [-32768, 32767]
pcm16Buffer[i] = Math.max(-0x8000, Math.min(0x7FFF, Math.round(pcm32Buffer[i] * 0x7FFF)));
}
// 2. 创建 WAV 缓冲区(44字节头 + PCM 数据)
const wavBuffer = new ArrayBuffer(44 + pcm16Buffer.byteLength);
const view = new DataView(wavBuffer);
// 3. 写入 WAV 头信息(固定格式,描述音频参数)
const writeString = (offset, str) => {
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
};
writeString(0, "RIFF"); // RIFF 标识
view.setUint32(4, pcm16Buffer.byteLength + 36, true); // 文件总大小
writeString(8, "WAVE"); // WAVE 标识
writeString(12, "fmt "); // 格式块标识
view.setUint32(16, 16, true); // 格式块大小(16 表示 PCM 格式)
view.setUint16(20, 1, true); // 音频格式(1 = PCM)
view.setUint16(22, numChannels, true); // 声道数
view.setUint32(24, sampleRate, true); // 采样率(与原始 PCM 一致)
// 字节率 = 采样率 × 声道数 × 位深/8
view.setUint32(28, sampleRate * numChannels * bitsPerSample / 8, true);
// 块对齐 = 声道数 × 位深/8
view.setUint16(32, numChannels * bitsPerSample / 8, true);
view.setUint16(34, bitsPerSample, true); // 位深
writeString(36, "data"); // 数据块标识
view.setUint32(40, pcm16Buffer.byteLength, true); // PCM 数据大小
// 4. 写入 PCM 数据
const pcmView = new Int16Array(wavBuffer, 44);
pcmView.set(pcm16Buffer);
return wavBuffer; // 返回 WAV 格式的二进制数据
}
// 步骤3:播放逻辑(WAV 解码 + 自动重采样)
playButton.addEventListener('click', async () => {
// 获取裸 PCM 数据
const pcm = await generatePcm();
// PCM 转 WAV(原始 PCM 采样率为 44100Hz)
const wavBuffer = pcmToWav(pcm, 44100);
// 创建目标音频上下文(基准采样率 16000Hz,触发自动重采样)
const audioContext = new AudioContext({ sampleRate: 16000 });
// 解码 WAV 并自动重采样为 16000Hz
const audioBuffer = await audioContext.decodeAudioData(wavBuffer);
// 播放
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
})
})()
</script>
</body>
自动采样率不影响时长,仅影响采样点数量
以 “时长固定为 6.912s 的单声道音频” 为例,不同采样率下 AudioBuffer 的核心参数遵循固定规律,印证 采样率不影响时长,仅影响采样点数量 的核心结论。
| 基准采样率 | 音频时长(audioBuffer.duration) | 采样点总数(audioBuffer.length) | 单声道采样数据长度(getChannelData(0).length) |
|---|---|---|---|
| 16000Hz | 6.912s(固定) | 110592(16000 × 6.912) | 110592(单声道场景下,与采样点总数一致) |
| 44100Hz | 6.912s(固定) | 304819(44100 × 6.912,四舍五入) | 304819(同上) |
- 采样率不影响音频时长:
audioBuffer.duration由 “总采样点数量 / 采样率” 计算得出,重采样时 “采样点数量” 与 “采样率” 同比变化,因此时长始终不变(如 110592 / 16000 = 304819 / 44100 ≈ 6.912s)。 - 采样率与采样点数量正相关:遵循公式
采样点总数 = 采样率 × 音频时长,采样率越高,单位时间内的采样点越多,能保留的高频细节(如乐器泛音)越丰富,但音频数据量也越大(存储 / 带宽消耗更高)。
手动重采样
除了 decodeAudioData 的自动重采样,也可通过代码手动实现重采样(如线性插值法),核心是通过 “插值计算” 在原采样点之间生成新采样点,实现采样率转换。
<body>
<button id="playBtn">播放</button>
<script>
(async () => {
const playButton = document.getElementById('playBtn')
// 步骤1:获取原始 PCM 数据(假设采样率 44100Hz)
async function generatePcm() {
const audioUrl = 'https://audio-1304256198.cos.ap-guangzhou.myqcloud.com/%E4%BA%B2%E5%88%87%E5%A5%B3%E5%A3%B0.mp3'
const response = await fetch(audioUrl)
const arrayBuffer = await response.arrayBuffer()
const tempAudioContext = new AudioContext()
const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer)
return {
pcm: audioBuffer.getChannelData(0),
sampleRate: audioBuffer.sampleRate // 记录原始采样率(避免硬编码)
}
}
// 步骤2:线性插值重采样函数
function transSamplingRate(data, fromRate, toRate) {
// 若采样率相同,直接返回原数据(避免无用计算)
if (fromRate === toRate) return new Float32Array(data);
// 1. 计算重采样后的数据长度(目标采样点数量)
const fitCount = Math.round(data.length * (toRate / fromRate));
const newData = new Float32Array(fitCount);
// 2. 计算“原始采样点”与“目标采样点”的映射比例
const springFactor = (data.length - 1) / (fitCount - 1);
// 3. 处理第一个采样点(直接复制,避免边界误差)
newData[0] = data[0];
// 4. 线性插值计算中间采样点
for (let i = 1; i < fitCount - 1; i++) {
const tmp = i * springFactor; // 目标采样点在原始数据中的“虚拟位置”
const before = Math.floor(tmp); // 虚拟位置左侧的原始采样点索引
const after = Math.ceil(tmp); // 虚拟位置右侧的原始采样点索引
const atPoint = tmp - before; // 虚拟位置在两个原始点之间的比例(0~1)
// 线性插值公式:new = 左点 + (右点 - 左点) × 比例
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
// 5. 处理最后一个采样点(直接复制,避免边界误差)
newData[fitCount - 1] = data[data.length - 1];
return newData;
}
// 步骤3:播放手动重采样后的 PCM
playButton.addEventListener('click', async () => {
// 获取原始 PCM 及采样率
const { pcm, sampleRate: originalRate } = await generatePcm();
// 手动重采样:44100Hz → 16000Hz
const transPcm = transSamplingRate(pcm, originalRate, 16000);
// 创建音频上下文(采样率与重采样后一致)
const audioContext = new AudioContext({ sampleRate: 16000 });
// 创建 AudioBuffer 并写入重采样后的 PCM
const audioBuffer = audioContext.createBuffer(
1, // 单声道
transPcm.length, // 重采样后的采样点数量
16000 // 与重采样后采样率一致
);
audioBuffer.getChannelData(0).set(transPcm);
// 播放
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
})
})()
</script>
</body>
- 优点:实现简单、计算量小,适合对实时性要求高的场景(如语音通话),能满足大部分基础音频处理需求。
- 缺点:高频信号还原精度较低(插值仅考虑相邻两个采样点,可能丢失细节),若需更高音质,可使用更复杂的算法(如 sinc 插值、立方插值)。
总结:自动重采样 vs 手动重采样
| 对比维度 | 自动重采样(decodeAudioData) | 手动重采样(线性插值) |
|---|---|---|
| 实现复杂度 | 低(API 自动处理) | 中(需手动实现插值逻辑) |
| 适用场景 | 编码音频格式(MP3、WAV)的采样率转换 | 裸 PCM 数据的采样率转换,或自定义需求 |
| 音质 | 较高(浏览器底层优化) | 中等(线性插值),可通过复杂算法提升 |
| 依赖格式 | 需编码音频格式(或 PCM 转 WAV) | 直接支持裸 PCM 数据 |
实际开发中,优先使用 decodeAudioData 的自动重采样(简单高效);若需处理裸 PCM 或自定义重采样逻辑,再考虑手动实现(如线性插值法)。
位深
32 位浮点转16 位整数
在PCM 转 WAV 流程中,32 位浮点 PCM 与 16 位整数 PCM 的转换是关键步骤,其本质是量化精度的适配——浏览器 Web Audio API 内部用 32 位浮点处理音频(保证动态范围),而 WAV 格式常用 16 位整数存储(平衡音质与文件大小),二者转换需严格遵循数值范围映射规则。
function float32ToInt16(float32Array) {
// 1. 初始化16位整数数组(长度与原PCM一致)
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
// 2. 范围钳位:确保浮点数值不超出[-1, 1](避免转换后超出16位整数范围)
const value = Math.max(-1, Math.min(1, float32Array[i]));
// 3. 数值映射:将[-1, 1]映射到[-32768, 32767]
// 负数:用 -32768(0x8000)作为基准,保证负数值范围完整
// 正数:用 32767(0x7FFF)作为基准,避免正数最大值溢出
int16Array[i] = value < 0 ? value * 0x8000 : value * 0x7FFF;
}
return int16Array;
}
16 位整数 转3 2 位浮点
当需要将 WAV 中的 16 位整数 PCM 重新导入 Web Audio API 处理时,需转回 32 位浮点,核心是 反向映射。
function int16ToFloat32(int16Array) {
const float32Array = new Float32Array(int16Array.length);
for (let i = 0; i < int16Array.length; i++) {
// 用 0x8000(32768)作为分母,统一映射到[-1, 1]
// 原因:16位整数的“数值跨度”是 65536(32768 - (-32768)),除以 32768 后正好覆盖[-1, 1]
float32Array[i] = int16Array[i] / 0x8000;
}
return float32Array;
}
声道
在Web Audio APi 开发过程中一般都是单声道,声道转换并不常见。
// 单声道(Float32Array)转立体声(2声道 AudioBuffer)
function monoToStereo(monoPcm, sampleRate) {
const audioContext = new AudioContext();
// 创建2声道缓冲区(长度、采样率与单声道一致)
const stereoBuffer = audioContext.createBuffer(2, monoPcm.length, sampleRate);
// 左声道、右声道均写入单声道数据
stereoBuffer.getChannelData(0).set(monoPcm); // 左声道
stereoBuffer.getChannelData(1).set(monoPcm); // 右声道
return stereoBuffer;
}