Dev Spec v2 · ← 返回文档首页

04 唱歌流水线

所有步骤实现同一接口(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(歌词识别与对齐)

  1. 用户粘贴了歌词 → 对干净的人声轨做强制对齐(逐句或逐字的时间戳)
  2. 没有粘贴 → 网关.stt 识别人声(唱歌的识别准确率比说话低),结果标注"识别结果,可能有误",用户可以在页面上修改后重新对齐
  3. 输出:带时间戳的歌词,供 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 分钟