跳到主要内容

16kHz 降采样

2025年02月15日
柏拉文
越努力,越幸运

一、认识


16kHz 音频降采样: 将 audioContext.sampleRate 指定的采样率转换为 16000Hz16kHz)。 并从基于 Linear Interpolation(线性插值)音频重采样(Resampling 算法优化为基于 Cubic Interpolation(三次插值) 的音频重采样算法。

为什么要降采样到 16kHz: 人类语音的主要信息集中在 300Hz - 3400Hz, 理论上 8kHz 采样率已足够, 16kHz 采样率可以捕捉到更多高频信息,使语音质量更自然、更清晰, 因此, 16kHz 足以保留语音信息,同时减少不必要的数据。另外, 音频文件的大小 = 采样率 × 量化位深 × 通道数 × 时长, 例如,假设 16-bit PCM 单声道音频, 44.1kHz 采样率(CD 级别):每秒数据量 44,100 × 16 / 8 = 88.2 KB/s, 而 16kHz 采样率(语音优化):每秒数据量 16,000 × 16 / 8 = 32 KB/s, 直接减少 63.7% 的数据量,而语音质量损失很小, 因此, 16kHz 采样率可以大幅减少数据量,提高存储和传输效率。最后, 16kHz 是语音识别模型的标准输入格式,避免额外转换和计算开销, 也是现代语音通讯的行业标准。

二、基于线性插值的音频降采样


2.1 认识

基于 Linear Interpolation(线性插值)音频重采样(Resampling 算法: 是一种简单高效的重采样算法, 但是只考虑两个相邻点,可能会导致高频损失和失真。具体逻辑为: 1. 将原始采样的音频数据转换为 Float32Array, data = new Float32Array(audioBuffer), 确保输入音频数据是 Float32Array 类型,方便处理。2. 计算目标采样点数, const fitCount = Math.round(data.length * (16000 / sampleRate)); 计算新的采样点数量, 并转换为 Float32Array3. 计算插值系数(springFactor const springFactor = (data.length - 1) / (fitCount - 1);, 用于计算原始数据和目标数据之间的比例关系(每个新采样点在原始数据中的映射位置)。4. 填充新数据(线性插值), 遍历 新数据的索引 i,找到 它在原始数据中的映射位置 tmp, before 是 向下取整 的索引,after 是 向上取整 的索引, atPoint 计算 新数据点在 beforeafter 之间的插值比例, 通过 线性插值公式: newData[i]=data[before]+(data[after]data[before])×atPoint\text{newData}[i] = \text{data}[\text{before}] + (\text{data}[\text{after}] - \text{data}[\text{before}]) \times \text{atPoint} 计算新数据点的值。5. 首尾数据直接赋值, 让首尾数据点不变,避免误差累积。

2.2 实现

export function to16kHz(audioBuffer, sampleRate = 44100) {
const data = new Float32Array(audioBuffer);
const fitCount = Math.round(data.length * (16000 / sampleRate));
const newData = new Float32Array(fitCount);
const springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];

for (let i = 1; i < fitCount - 1; i++) {
const tmp = i * springFactor;
const before = Math.floor(tmp).toFixed();
const after = Math.ceil(tmp).toFixed();
const atPoint = Number(tmp) - Number(before);
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}

newData[fitCount - 1] = data[data.length - 1];
return newData;
}

三、基于三次插值的音频降采样


3.1 认识

基于 Cubic Interpolation(三次插值)音频重采样 (Resampling 算法: 则考虑 前后四个点,能更好地平滑过渡,减少失真,提高音质。1. 将原始采样的音频数据转换为 Float32Array, data = new Float32Array(audioBuffer), 确保输入音频数据是 Float32Array 类型,方便处理。2. 计算目标采样点数, const fitCount = Math.round(data.length * (16000 / sampleRate)); 计算新的采样点数量, 并转换为 Float32Array3. 定义 Cubic Interpolation 计算方法: 使用三次插值公式计算新数据点的值, 公式为 F(x)=P1+0.5x(P2P0+x(2P05P1+4P2P3+x(3(P1P2)+P3P0)))F(x) = P_1 + 0.5x (P_2 - P_0 + x (2P_0 - 5P_1 + 4P_2 - P_3 + x (3(P_1 - P_2) + P_3 - P_0)))4. 遍历新数组,进行插值计算: 计算当前点在原始数据中的 映射位置 tmp, 找到最近的四个数据点(p0, p1, p2, p3), 使用 cubicInterpolate 计算新数据点值。5. 边界处理: p0 不能小于索引 0,否则取 data[0], p3 不能超过 data.length - 1,否则取 data[data.length - 1]

3.2 实现

export function to16kHz(audioBuffer, sampleRate = 44100) {
const data = new Float32Array(audioBuffer);
const fitCount = Math.round(data.length * (16000 / sampleRate));
const newData = new Float32Array(fitCount);
const springFactor = (data.length - 1) / (fitCount - 1);

function cubicInterpolate(p0, p1, p2, p3, x) {
return (
p1 +
0.5 *
x *
(p2 -
p0 +
x * (2 * p0 - 5 * p1 + 4 * p2 - p3 + x * (3 * (p1 - p2) + p3 - p0)))
);
}

for (let i = 0; i < fitCount; i++) {
const tmp = i * springFactor;
const index = Math.floor(tmp);
const x = tmp - index;

// 取出四个相邻的数据点,处理边界情况
const p0 = data[Math.max(0, index - 1)];
const p1 = data[index];
const p2 = data[Math.min(index + 1, data.length - 1)];
const p3 = data[Math.min(index + 2, data.length - 1)];

newData[i] = cubicInterpolate(p0, p1, p2, p3, x);
}

return newData;
}