目录
[二、频谱定位:不要假设低频一定在 200 Hz 以下](#二、频谱定位:不要假设低频一定在 200 Hz 以下)
[五、最终方案:只改 0.18 秒的完整音轨](#五、最终方案:只改 0.18 秒的完整音轨)
本文面向用 AI 生成歌曲并合成视频的开发者。问题很具体:同一首歌的第一段副歌正常,第二段副歌在"每一次上钩"之后多出一声低频"咕",重新渲染无效。下面记录从定位、替换到缓存的完整排查过程,示例中的路径、时间点和服务器地址均已泛化。

一、先排除渲染与封装
这套 MV 的合成方式是:视频段先渲染成无声 mp4,再用 ffmpeg 拼接,最后把整首歌作为一条音轨混进去。分幕试看则是每幕单独切歌、单独封装。因此,"某一幕有异响"不一定来自渲染,也可能来自原曲对应片段。
第一步是重渲染同一幕,并比对文件哈希:
bash
sha256sum 07_chorus2.mp4
# 重新渲染后再次比对
sha256sum 07_chorus2.mp4
两次哈希完全一致。渲染器是确定性的,画面帧和音频封装都没有变化,可以排除视频编码和封装环节。问题只剩两个方向:
- 原曲在对应时间点本身有异常;
- 播放链路拿到了旧文件,改动没有真正生效。
二、频谱定位:不要假设低频一定在 200 Hz 以下
先用 ffmpeg 导出频谱图,确认异常的大致时间:
bash
ffmpeg -y -ss 112.5 -t 5 -i song.mp3 \
-lavfi "showspectrumpic=s=1600x700:mode=combined:scale=log" spec.png
频谱图能看到异常点,但要精确量化,按频段计算短时能量更可靠。下面这段脚本按 10 ms 帧统计多个频段的能量,并打印相对局部中位数的偏差:
python
import numpy as np
from scipy.io import wavfile
from scipy import signal
sr, x = wavfile.read("song.wav")
x = x.astype(np.float32) / 32768.0
if x.ndim == 2:
x = x.mean(axis=1)
f, t, Z = signal.stft(x, fs=sr, nperseg=512, noverlap=448, boundary=None)
mag = np.abs(Z) ** 2
bands = [(50, 120), (120, 200), (200, 400), (300, 800), (800, 2000), (2000, 5000)]
E = np.array([mag[(f >= lo) & (f < hi)].sum(axis=0) for lo, hi in bands])
db = 10 * np.log10(E + 1e-12)
第一轮只扫了 200 Hz 以上,发现 113.9 s 附近有 300--2000 Hz 的宽带瞬态,第二段副歌比第一段高约 8 dB。但把 200--2400 Hz 压掉 16 dB 后,异响仍然存在。
继续往下扫 50--400 Hz,才看到真正漏掉的部分:114.21 s 附近有一个 120--200 Hz 的短促爆破。前面的滤波器从 200 Hz 开始,正好把这个频段绕过去了。
| 频段 | 异常前 | 异常点 | 处理后 |
|---|---|---|---|
120--200 Hz |
-40 dB |
-20 dB |
-40 dB |
800--2000 Hz |
-17 dB |
-9 dB |
-31 dB |
这里的教训很直接:"低频"不等于 200 Hz 以下,也不等于只压一个频段。先按倍频程或固定带宽分频段扫描,再决定滤波范围。
三、缓存:为什么改了却听不到
排查过程中还有一个独立问题:连续几版修复,本地文件哈希都变了,但播放器听起来没有变化。检查 Nginx 配置后发现,只有 html/css/js/图片 设置了 Cache-Control: no-cache,mp4 和 mp3 不在规则里:
nginx
location ~* \.(webm|png|jpg|jpeg|css|js|html)$ {
add_header Cache-Control "no-cache";
}
mp4 没有显式缓存头,浏览器会按启发式规则缓存;同一个 URL 反复打开时,很可能一直在播放旧文件。修复方式是给媒体文件单独加规则:
nginx
location ~* \.wav$ {
default_type audio/wav;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
location ~* \.(mp4|mp3|m4a|webm)$ {
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
改完后先测试配置,再重载:
bash
nginx -t && systemctl reload nginx
curl -sI http://<服务器IP>/preview/acts/07_chorus2.mp4 | grep -i cache-control
调试媒体问题时,先确认浏览器拿到的是新文件,否则后面的音频处理都可能是白做。
四、片段替换:接缝比异响更难处理
异响消失不等于结束。把出问题的 0.6 s 整段换成另一段副歌的同位置音频后,"咕"没有了,但接缝明显不流畅。原因主要有三个:
- 线性交叉淡化会掉电平 。两段不同录音不相关,线性增益在中间会掉
3--5 dB,听起来像"断一下"。 - 咬字时间不同。两段副歌虽然歌词相同,但音节间隔并不一致,直接平移会对不齐。
- 电平不同。替换段的短时 RMS 和原句不一致,接缝处会出现忽大忽小。
对应的处理方式是:等功率淡化、局部对齐、包络匹配。
等功率淡化用 sin/cos 曲线,保证两段不相关信号叠加时总功率基本恒定:
python
import numpy as np
u = np.linspace(0, 1, n_fade)[:, None]
fade_in = np.sin(u * np.pi / 2)
fade_out = np.cos(u * np.pi / 2)
block[:n_fade] = original[:n_fade] * fade_out + replacement[:n_fade] * fade_in
block[-n_fade:] = replacement[-n_fade:] * fade_out + original[-n_fade:] * fade_in
局部对齐可以用人声频段的包络互相关,找一个几十毫秒以内的偏移;包络匹配则用短时 RMS 计算增益曲线,让替换段的强弱跟随原句:
python
from scipy.ndimage import uniform_filter1d
env = np.sqrt(uniform_filter1d(mono ** 2, int(0.04 * sr), mode="nearest"))
e_orig = uniform_filter1d(env[i_orig:i_orig + n], int(0.08 * sr), mode="nearest")
e_repl = uniform_filter1d(env[i_repl:i_repl + n], int(0.08 * sr), mode="nearest")
gain = np.clip(e_orig / (e_repl + 1e-6), 0.7, 1.6)
replacement *= gain[:, None]
还有一个容易忽略的点:替换窗口不要切在字头。这次的问题出在"都唱给你听"的"都"上------替换起点原本放在"都"之前,结果把字头切掉,听起来像换了一口气。把起点往后挪到"都"之后,保留原字头,只在它后面接替换段,衔接才正常。
五、最终方案:只改 0.18 秒的完整音轨
最终没有替换整段副歌,而是生成一条完整长度的修复音轨 ,只改 113.96--114.14 s 这 0.18 s,其他采样点保持不变。这样既消除了异响,也不会改变整首歌的结构和字幕时间。
分幕试看视频用这条音轨的对应片段重新封装:
bash
ffmpeg -y -i act_video.mp4 \
-ss 111.4 -t 19.1 -i song_fixed.wav \
-map 0:v:0 -map 1:a:0 \
-c:v copy -c:a aac -b:a 192k -shortest act_fixed.mp4
最终合成时,直接把整条修复音轨交给混音步骤,而不是只改某一幕的分段音频。这样试看和成片使用的是同一份音频,不会出现"试看正常、成片还有"的情况。
另外,原曲保留不动,修复音轨单独命名;如果最终还要再编码一次,优先用无损 WAV 作为中间文件,避免多一次 MP3 有损编码。
六、经验清单
- 先比对
SHA-256,确认问题是否真的来自渲染。 - 频谱定位要分频段扫描,不要假设"低频"只在
200 Hz以下。 - 改完媒体文件先检查
Cache-Control,确认播放器拿到的是新文件。 - 交叉淡化用等功率曲线,避免中间掉
3--5 dB。 - 替换前做局部时间对齐和短时 RMS 匹配,不要直接平移。
- 替换窗口避开字头、字尾,优先在音节边界落刀。
- 修复音轨独立保存,最终合成与分幕试看共用同一份音频。
小结:这次"咕"声的根因是原曲第二段副歌的低频重音比第一段更强,而排查过程被缓存、频段遗漏和替换接缝三个问题反复干扰。把定位、缓存、接缝拆开处理之后,修复范围可以缩小到不到 0.2 s,整首歌的其余部分完全不动。
标签:音频处理 FFmpeg Python Nginx AI 视频
背景说明:本文来自一次 AI 生成歌曲 MV 的后期修复实践,示例中的服务器地址、文件路径和项目名称已做泛化处理;频谱数据来自实际测量,未使用虚构截图或外部引用。