重采样时音调偏移的根本原因是混淆“变速”与“变调”,正确做法需通过STFT、时间轴插值、相位校正和ISTFT实现变速不变调,而非简单调整playbackRate或依赖MediaRecorder。

重采样时音调偏移,根本原因是把“变速”和“变调”混为一谈了。浏览器原生的 playbackRate 或简单线性重采样,只是按比例拉伸或压缩采样点——快放=采样点挤得更密=频率整体上移=音调变高;慢放则相反。而真正要“变速不变调”,必须在时频域里动手脚,把时间维度和频率维度拆开处理。
重采样 ≠ 简单缩放采样点
当你用 AudioBufferSourceNode.playbackRate = 1.5 播放一段语音,它不会让说话变快但音色不变,而是直接把整个频谱右移 50%,男声可能变成女声。DJ台、语音识别(如 SenseVoiceSmall)、在线播客变速功能,都要求语速可调而基频稳定,这就绕不开相位声码器(Phase Vocoder)这类算法。
关键不是“怎么降采样”,而是“怎么保音高”
立即学习“前端免费学习笔记(深入)”;
比如你有一段 44.1kHz 录音,想喂给只接受 16kHz 输入的语音识别模型。硬切采样率会失真,正确做法是:
- 先做短时傅里叶变换(STFT),把音频转成帧+频谱+相位
- 对时间轴做插值或删减(实现变速/降帧率),但每帧内的幅值谱保持不变
- 对相位做导数补偿(unwrap + phase locking),防止拉伸后相位跳变导致“金属感”失真
- 再用逆变换(ISTFT)合成回时域波形
这个过程不能靠 AudioContext.sampleRate 设置解决——它是只读的,仅反映当前设备能力;也不能靠 MediaRecorder 的 mimeType 参数绕过——它不控制输入流采样率,只影响编码输出。
避开音调偏移的实操路径
- ✅ 用 AudioWorklet 实现自定义重采样:取代已废弃的 ScriptProcessorNode,在独立线程里跑 STFT → 相位校正 → 时间重采样 → ISTFT 流程
- ✅ 借力成熟库:Tone.js、standardized-audio-context、or WebAudioControls 提供封装好的 timeStretch 方法,内部已实现相位连续性处理
- ✅ 输入端就对齐目标采样率:用 getUserMedia 获取流后,立即接入 AudioContext,通过 OfflineAudioContext 渲染+重采样生成新 AudioBuffer,再送入识别模型或播放链路
- ❌ 避免直接改 playbackRate 做“伪变速”:尤其在语音场景下,哪怕只调 1.2 倍,元音共振峰也会漂移,导致识别率断崖下降
- ❌ 不依赖 MediaRecorder 输出采样率“碰运气”:Chrome 可能输出 48kHz,Safari 可能固定 44.1kHz,这不是可控路径
特别注意移动端与识别模型的配合
SenseVoiceSmall 等轻量语音模型明确要求 16kHz 单声道 PCM 输入。若你用 MediaRecorder 录制 audio/webm;codecs=opus,虽然体积小、带宽低,但解码后实际采样率未必是 16kHz——Opus 编码器会动态适配,需用 ffmpeg.wasm 或 WebAssembly 版 libresample 在前端做二次转码,再喂给模型。
不复杂但容易忽略



















