音频变速的两条路:线性插值重采样,与 2048 采样窗的 WOLA 时间拉伸
变速先问一句:变不变调
倍速播放音频有两种截然不同的结果。把 2 倍速的播客听成「花栗鼠」,那是音高跟着速度一起走了;听感自然、只是语速变快,那是时间拉伸在起作用。两条路的实现难度差一个量级,先从简单那条说起。
重采样(音高随速):音频是一串按固定采样率排队的数值,2 倍速不过是每隔 2 个采样取 1 个来播。输出长度变一半、频率成分整体翻倍——波形被压缩,音调自然升高。实现是十几行循环:目标位置 pos = i × rate,落在两个原始采样之间,用线性插值取中间值补上。它等价于 <audio> 元素的 playbackRate(默认 preservesPitch 关闭时),浏览器播放器里的倍速就是这么来的。优点是 O(n) 秒出结果、无损语义(对采样值只插值不分析);缺点是声音变形,超过 1.5 倍人声就明显发尖。
时间拉伸(变速不变调):难点在于语速和音高在波形里不是两个独立旋钮。基频由声波周期决定,想周期不变、又把时间轴压缩,只能把信号切碎、挪位置重新拼接——搬移的是「内容的排列」,不是波形本身。
颗粒搬运:切、加窗、重叠、归一
时间拉伸的主流做法是重叠相加(Overlap-Add):把源信号切成固定长度的颗粒(grain),输出侧按固定步进(hop)逐个摆放,源侧步进则按倍速拉开——倍速 >1 时源侧每步跨得更远,同样的内容被更紧凑地搬到输出里,总长变短,速度就上去了。每个颗粒的原始采样周期没动,音高自然保住。
但硬切再拼会在接缝处产生爆音:颗粒边界两侧波形值不连续,跳变本身就是宽带噪声。解法是给每个颗粒加 Hann 窗——两端平滑过渡到零的钟形窗,接缝处的电平被压到零,跳变消失。窗的参数是一组连环取舍:窗太短(几百采样),颗粒小到失去频率分辨率,声音发「相位器」味;窗太长(几万采样),搬移粒度太粗,快速变化的音节在窗内自相混合。2048 采样在 48kHz 下约 43ms——一个音节的量级,是语音与音乐通吃的经验值。
重叠密度是另一处关键:hop = grain/4 意味着输出里任意时刻有 4 个窗同时叠加。为什么要叠这么多?单个 Hann 窗中段高两端零,不重叠的话输出音量会按窗形周期起伏,像坏了的调音台。4 倍重叠让多个窗的隆起互相填平——但这里有个教科书常略过的细节:Hann 窗以 hop = grain/4 叠加的窗权和约等于 2.0,并不恰好是常数(中段仍有极小的周期波动)。若直接叠加不管权和,输出音量带着千分之几的周期性起伏;正确做法是叠加的同时累计每个输出采样被覆盖的窗权之和,最后逐点除以权和归一化(WOLA,加权重叠相加)。归一化做到位有个妙用:1.0 倍速跑完整条流水线,输出与输入逐点相同——变换在单位速率下透明,这是检验实现正确性的最硬指标。
| |
参数边界与工程兜底
倍速限幅 0.5–3.0 不是随口画的:低于 0.5 时源侧步进超过四个窗长,颗粒之间的内容被整段跳过,输出会出现空洞感;高于 3 时相邻颗粒几乎重叠取材,快速语音糊成一团。短于一个窗的输入是必须单列的边界——整段当一个颗粒原样输出,不做搬移。
输出长度的口径同样要精确到采样:输出 = 末窗起点 + 窗长,与「原长 ÷ 倍速」的差被窗长兜住,误差上界一个 grain。时长、体积、耗时预估共用这一套长度公式,界面上显示的「预计新时长 3 分 20 秒」和最终导出的文件才能对得上。
浏览器里的分工
🔧 音频变速
把两条路做成显式选项:「倍速直变(重采样)」秒出、音高随速;「变速不变调(重叠窗时间拉伸)」跑 WOLA 全流程。试听也按两条路分开:处理结果按 1.0 倍速播——新节奏已经写进采样,所见即所得;「实时试听原音频」则直接用 AudioBufferSourceNode.playbackRate,与播放器倍速同一机制,方便对比两种结果挑合适的。
同一条「解码 → 逐窗统计 → 重排采样」的骨架还撑着🔧 口播静音切除 (详见静音检测算法拆解)与 🔧 音频裁剪 ;而 dBFS 这本响度账,WebAudio 响度刻度里算过。这一整族工具,连同录音、混音、BPM 检测,都收在音频创作工具箱专题里,全部本地计算,素材不出浏览器。