所有步骤实现同一接口(media/audio/steps):
class Step(Protocol):
name: str; version: str
queue: Literal["snd.cpu", "snd.gpu"]
def run(self, inputs: dict[str, Path], params: dict, workdir: Path) -> StepResult: ...
@dataclass
class StepResult:
outputs: dict[str, Path]
metrics: dict
中间文件统一为 48kHz / 32 位浮点 WAV。
1. probe
ffprobe + ffmpeg ebur128 测量:时长、采样率、声道、积分响度、真峰值、是否有视频流。时长 < 10 秒或 > 10 分钟(唱歌模式)→ 失败并提示。
2. separate(GPU)
| 用途 |
输出 |
| 原曲 → 伴奏 + 原唱人声 |
backing、ref_vocal |
| 用户上传的人声混有伴奏 |
提取 vocal |
| 工具箱人声分离 |
2 轨 / 4 轨 |
- 实现:audio-separator(RoFormer 系列)为主,Demucs
htdemucs_ft 为备选;每个模型权重都要单独核查许可证
- 原唱人声再过一次去混响 / 去和声模型,得到更干净的旋律参考(只用于音高提取,不输出给用户)
- 验收:内部盲听 10 首,人声残留和伴奏漏声的主观评分 ≥ 4/5;4 分钟歌曲 ≤ 60 秒
3. denoise(仅人声轨)
DeepFilterNet,唱歌模式默认 light(最大衰减 6 dB),避免损伤气声和尾音。
4. pitch_track + align
| 步骤 |
实现 |
| 音高提取 |
抗伴奏残留的深度学习音高检测模型(RMVPE、CREPE 一类,许可证逐个核查);每 10ms 一帧,输出 f0 和有声/无声判断 |
| 时间对齐 |
以色度特征 + f0 为特征做 DTW;搜索带宽 ±2 秒;输出"用户时间 → 原唱时间"的映射 |
| 对齐质量 |
对齐路径代价过高(阈值用测试曲校准)→ 视为"用户唱的不是这首歌或者改编太多",自动改用"按调内音修正"模式,并在结果页说明 |
5. tune(参考旋律修音)
5.1 为什么不用"吸附到最近音"
| 问题 |
后果 |
| 不知道歌手本来想唱哪个音 |
唱偏超过半个音时会被吸附到错误的音上,越修越难听 |
| 按帧吸附 |
颤音和滑音被拉平,声音发"机器味" |
| 依赖调性判断 |
调性判断出错时,整首都修错 |
5.2 流程
f0_user ──按音符分段──▶ 用户音符(稳定段:相邻帧差 < 50 音分且持续 ≥ 80ms)
│
对齐映射 ──▶ 每个用户音符对应的原唱片段 ──▶ 原唱音符中心音高
│
偏差 = 原唱中心 − 用户中心(音分)
· 八度折叠:相差接近 ±1200 音分时按同一个音处理(用户比原唱低或高一个八度)
· |偏差| > 250 音分 → 视为对齐错误或有意改编,该音符不修
· 滑音段、无声段不修
│
新 f0 = 用户 f0 + strength × 偏差(整个音符平移,保留音符内部的颤音和起伏)
音符边界 40ms 平滑过渡
keep_vibrato=false 时,额外把音符内部的波动压缩 50%
│
WORLD 声码器(pyworld):保留频谱包络和非周期成分,只替换 f0,重合成
| 参数 |
默认 |
范围 |
tune_strength |
0.7 |
0~1 |
keep_vibrato |
true |
|
reference |
original(有原曲时) |
original / key |
key |
自动检测 |
用户可以手动指定 |
5.3 没有原曲时(reference=key)
检测调性(或由用户指定)→ 同样按音符修正到调内最近的音(不是按帧吸附)→ 页面说明:唱偏超过半个音的地方可能被修到相邻的音上。
5.4 自检
| 检查 |
失败时 |
| 修正后与目标旋律的平均偏差没有下降 |
降低强度重跑一次;仍然没有改善就输出未修音版本,并提示 |
| 出现八度跳变 |
该音符还原为原始音高 |
| 重合成后出现明显的"电流声"(能量突变检测) |
该段还原 |
6. timing(节奏对齐,P2)
- 利用对齐映射,找出与原唱起始时间偏差 > 80ms 的音符
- 对这些音符做局部时间伸缩(保持音高不变),伸缩比例限制在 0.8~1.25
- 其余部分不动
7. vocal_chain(按曲风的人声处理链)
| 曲风 |
处理链(顺序) |
| 流行 pop |
高通 90Hz → 去浑浊(300Hz 附近动态衰减)→ 压缩 3:1 → 去齿音 → 轻度饱和 → 清晰度提升(3~5kHz 小幅)→ 板式混响(发送)+ 1/8 拍延迟(发送) |
| 抒情 ballad |
高通 80Hz → 压缩 2.5:1 → 去齿音 → 更长的厅堂混响 → 延迟较少 |
| 说唱 rap |
高通 100Hz → 压缩 4:1 → 去齿音 → 饱和稍多 → 混响很少,偏干 |
| 摇滚 rock |
高通 100Hz → 压缩 4:1 → 饱和 → 中频提升 → 短房间混响 |
| 国风 |
高通 80Hz → 压缩 2.5:1 → 去齿音 → 空气感提升(10kHz 以上)→ 较长混响 + 延迟 |
- 实现:Python 端用成熟的开源音频效果库(例如 Spotify 的 pedalboard,GPL-3.0,只在服务器端运行)或 ffmpeg 滤镜链
- 参数不写死在代码里,存在
presets/ 中带版本号,改动后跑回归测试
- 压缩器的阈值根据人声的实际响度自动设定(先测量,再按"目标压缩量约 3~6 dB"反推)
8. mix(自动混音)
| 步骤 |
做法 |
| 响度比例 |
分别测量人声和伴奏的短时响度,按曲风的目标差值设定增益(例如流行:人声比伴奏高 1~3 LU;说唱:高 3~5 LU) |
| 频段避让 |
在人声出现的时间段,把伴奏的 2~5kHz 动态衰减 1~3 dB(侧链控制) |
| 时间对齐 |
用户上传的人声和伴奏如果有整体偏移,用互相关自动对齐(浏览器录音另有延迟补偿) |
| 声像 |
人声居中;伴奏保持原样 |
验收:盲听"人声清楚又不突兀" ≥ 80%。
9. master
| 情况 |
做法 |
| 用户提供参考曲 |
matchering(GPL-3.0,只在服务器端运行) |
| 没有参考曲 |
按预设:流媒体 −14 LUFS / −1 dBTP;短视频 −14 LUFS / −1 dBTP |
| 后检查 |
再测一次:真峰值超标 → 降低 1 dB 重跑 |
10. lyrics(歌词识别与对齐)
- 用户粘贴了歌词 → 对干净的人声轨做强制对齐(逐句或逐字的时间戳)
- 没有粘贴 → 网关.stt 识别人声(唱歌的识别准确率比说话低),结果标注"识别结果,可能有误",用户可以在页面上修改后重新对齐
- 输出:带时间戳的歌词,供 06 的视频渲染使用
11. encode / label
| 格式 |
参数 |
| mp3 |
320kbps(付费)/ 192kbps(免费) |
| wav |
48kHz / 24 位(付费) |
| 分轨 |
修音后的干声、处理后的人声、伴奏(付费;仅限授权伴奏作品) |
| 发布安全版人声 |
处理后的人声(wav / m4a),开头对齐到原曲的第一拍,附 offset_ms;原曲伴奏作品唯一的导出方式 |
导出校验:renders.backing_source = original_separated 时,编码步骤只生成发布安全版人声,并在生成后做一次检查:把导出的人声和分离出的伴奏做互相关,相关度超过阈值(说明混入了伴奏)则判定失败、不交付。
所有输出写入隐式标识(processing=tune,mix,master)。
12. 性能预算(4 分钟歌曲)
| 步骤 |
目标耗时 |
| 分离(GPU) |
≤ 60 秒 |
| 降噪 + 音高提取 + 对齐 + 修音 |
≤ 45 秒(CPU) |
| 人声链 + 混音 + 母带 |
≤ 20 秒 |
| 歌词对齐 |
≤ 20 秒 |
| 视频渲染 |
≤ 60 秒 |
| 合计 |
P95 ≤ 3 分钟 |