效率工具

Adobe Podcast:Enhance Speech 是针对人声的处理,如何把背景音乐安全加回来

Enhance Speech 是针对人声的处理:官方 FAQ 说明它会过滤噪声和杂音。如果录音里混着背景音乐,音乐很可能被一起削弱(这是我根据它的定位做的推断)。 稳妥的做法是:只增强纯人声,再在本地把音乐加回来。这篇做 remix.py:根据测量结果放置音乐音量,人声出现时自动压低音乐(ducking),最后用两遍 loudnorm 达到目标响度。

BINBHO · 图文教程

Enhance Speech 是针对人声的处理:官方 FAQ 说明它会过滤噪声和杂音。如果录音里混着背景音乐,音乐很可能被一起削弱(这是我根据它的定位做的推断)。 稳妥的做法是:只增强纯人声,再在本地把音乐加回来。这篇做 remix.py:根据测量结果放置音乐音量,人声出现时自动压低音乐(ducking),最后用两遍 loudnorm 达到目标响度。

先看清成品

  • 最终混音:−15.9 LUFS(目标 −16)、真峰值 −1.8 dBTP。
  • Ducking 生效:音乐在停顿时约 −39 dB,人声出现时压低到约 −54 dB。
  • 修复了两个问题:两遍 loudnorm 仍然只有 −16.4(缺少峰值余量);音乐几乎听不见(固定增益和音乐文件本身的音量有关)。pytest 4 项通过。

合规提醒:Adobe Podcast 只能在官方网页中使用(https://podcast.adobe.com),没有公开 API。本文的脚本只在本地处理音频,上传和下载都在网页中手动完成,不自动化操作网页,不共享 Adobe 账号。录制他人(嘉宾、采访对象)之前要取得同意。背景音乐要用你有权使用的素材;示例中的音乐是我用正弦波合成的。本文不列价格,套餐限制以官网为准。

运行说明:box 上没有 Adobe 账号,没有把文件上传到 Enhance Speech。所有脚本和测试都在 box 上真实运行过(ffmpeg 7.1.5、espeak-ng、Python 3.13、numpy 2.5.4)。示例人声是 espeak-ng 合成后加上噪声和回声得到的;B、C 篇中的“增强后”文件是我自己的 ffmpeg 处理链(standin_enhance.sh),不是 Adobe 的输出。

做完之后你会得到什么?

成果 说明
remix.py 混入音乐,并归一到目标响度
make_music.sh 合成测试背景音乐
measure.py 与 B 篇相同
test_remix.py 4 项测试

需要准备哪些环境?

项目 说明
ffmpeg / ffprobe box 上是 7.1.5;Debian/Ubuntu 用 apt install ffmpeg
espeak-ng 只用于生成测试音频
Python 3.9 以上,pip install numpy pytest
浏览器 Chrome、Safari、Edge、Firefox 的最新版(官方 FAQ)

关键事实

事实 来源
Enhance Speech 会过滤噪声和杂音、调整音高和音量,并做归一化 FAQ
loudnorm 两遍法:第一遍测量,第二遍带 measured_* 参数,并设 linear=true FFmpeg loudnorm 滤镜文档
sidechaincompress:用第二路输入(人声)控制第一路(音乐)的压缩 FFmpeg 滤镜文档

目标响度 −16 LUFS 和真峰值 −1.5 dBTP 是播客常用的数值,不是 Adobe 的要求;可以用 --target 修改。

文件结构

68-adobe-podcast-C-src/
├── remix.py
├── measure.py
├── make_music.sh
├── make_audio.sh
├── standin_enhance.sh
├── conftest.py
└── test_remix.py

第 1 步:合成背景音乐

#!/usr/bin/env bash
# 生成测试用的背景音乐(我自己合成的 A 大三和弦长音 + 轻微颤音),避免使用任何受版权保护的音乐。
set -euo pipefail
ffmpeg -loglevel error -y -f lavfi -i "sine=f=220:d=26" -f lavfi -i "sine=f=277.18:d=26" -f lavfi -i "sine=f=329.63:d=26" \
  -filter_complex "[0][1][2]amix=inputs=3:normalize=0,volume=0.2,tremolo=f=0.5:d=0.3,afade=t=in:d=2,afade=t=out:st=23:d=3,aresample=48000" -ac 1 samples/music_bed.wav

第 2 步:混音脚本

"""把背景音乐加回到增强后的人声里:Enhance Speech 会把音乐当成噪声处理(官方说明它针对人声),
所以应该先增强纯人声,再混入音乐。处理方式:
1. 测量人声和音乐的响度,把音乐放到比人声低 --bed-db LU 的位置(默认 15),用人声做侧链压缩,人声出现时音乐自动再压低(ducking)
2. 两遍 loudnorm:第一遍测量,第二遍按测量值线性归一到目标响度(--target,默认 -16 LUFS,这是常用值,不是 Adobe 的要求)
依赖 ffmpeg。"""
import argparse, json, re, subprocess, sys
from measure import ebur128

# 限幅到约 -4 dBFS(0.63),给第二遍的线性增益留出余量;否则 loudnorm 为了守住真峰值会自动退回动态模式,响度达不到目标
LIMIT = 0.63

def run(args): return subprocess.run(["ffmpeg", "-hide_banner", "-nostats", "-y", *args], capture_output=True, text=True)

def bed_gain(voice, music, bed_db):
    """让音乐的响度比人声低 bed_db LU,和音乐文件本身有多响无关。
    (第一版直接 volume=-12dB:我合成的音乐本身只有约 -32 dB,结果停顿时音乐也只剩 -51 dB,几乎听不见。)"""
    return round(ebur128(voice)["lufs"] + bed_db - ebur128(music)["lufs"], 2)

def mix_graph(gain_db, extra=""):
    return (f"[1:a]volume={gain_db}dB[bed];[0:a]asplit=2[v][key];"
            f"[bed][key]sidechaincompress=threshold=0.02:ratio=8:attack=20:release=400[duck];"
            f"[v][duck]amix=inputs=2:duration=first:normalize=0,alimiter=limit={LIMIT}:level=false{extra}")

def remix(voice, music, out, target=-16.0, tp=-1.5, bed_db=-15):
    g = bed_gain(voice, music, bed_db)
    r = run(["-i", voice, "-i", music, "-filter_complex", mix_graph(g, f",loudnorm=I={target}:TP={tp}:LRA=11:print_format=json"), "-f", "null", "-"])
    m = json.loads(r.stderr[r.stderr.rfind("{"):r.stderr.rfind("}") + 1])
    second = (f",loudnorm=I={target}:TP={tp}:LRA=11:measured_I={m['input_i']}:measured_TP={m['input_tp']}:"
              f"measured_LRA={m['input_lra']}:measured_thresh={m['input_thresh']}:offset={m['target_offset']}:linear=true")
    r2 = run(["-i", voice, "-i", music, "-filter_complex", mix_graph(g, second), "-ar", "48000", out])
    if r2.returncode: raise SystemExit(r2.stderr[-800:])
    return m

def ducked_music(voice, music, out, bed_db=-15):
    """单独导出被压缩后的音乐轨,用于检查 ducking 是否生效。"""
    run(["-i", voice, "-i", music, "-filter_complex",
         f"[1:a]volume={bed_gain(voice, music, bed_db)}dB[bed];[bed][0:a]sidechaincompress=threshold=0.02:ratio=8:attack=20:release=400", "-ar", "48000", out])

if __name__ == "__main__":
    ap = argparse.ArgumentParser(); ap.add_argument("voice"); ap.add_argument("music"); ap.add_argument("out")
    ap.add_argument("--target", type=float, default=-16.0); ap.add_argument("--bed-db", type=float, default=-15, help="音乐比人声低多少 LU")
    a = ap.parse_args(); m = remix(a.voice, a.music, a.out, a.target, bed_db=a.bed_db)
    print("第一遍测量:", {k: m[k] for k in ("input_i", "input_tp", "target_offset")}); print("写入", a.out)

第 3 步:测试

import numpy as np, pytest
import measure as M, remix as R

@pytest.fixture(scope="module")
def out(tmp_path_factory):
    d = tmp_path_factory.mktemp("r"); f = str(d / "final.wav"); duck = str(d / "duck.wav")
    R.remix("samples/voice_standin.wav", "samples/music_bed.wav", f); R.ducked_music("samples/voice_standin.wav", "samples/music_bed.wav", duck)
    return f, duck

def test_loudness_target(out):
    # 调试时:只用两遍 loudnorm 时结果是 -16.4(真峰值不够余量,自动退回动态模式);加 alimiter 后为 -15.9
    r = M.measure(out[0])
    assert abs(r["lufs"] - (-16)) <= 0.5 and r["true_peak"] <= -1.0

def test_ducking(out):
    _, vdb = M.frames("samples/voice_standin.wav"); _, mdb = M.frames(out[1]); n = min(len(vdb), len(mdb))
    vdb, mdb = vdb[:n], mdb[:n]; speech = vdb > np.percentile(vdb, 60); pause = vdb < np.percentile(vdb, 20)
    pause &= mdb > -60                                   # 排除音乐淡入淡出的静音部分
    assert mdb[pause].mean() - mdb[speech].mean() > 3   # 有人声时音乐至少低 3 dB

def test_custom_target(tmp_path):
    f = str(tmp_path / "f.wav"); R.remix("samples/voice_standin.wav", "samples/music_bed.wav", f, target=-19)
    assert abs(M.measure(f)["lufs"] + 19) <= 0.5

def test_bed_gain_independent_of_music_level(tmp_path):
    # 调试时发现:固定 volume=-12dB 时,音乐的最终音量取决于音乐文件本身有多响
    import subprocess
    loud = str(tmp_path / "loud.wav")
    subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", "samples/music_bed.wav", "-af", "volume=10dB", loud], check=True)
    g1 = R.bed_gain("samples/voice_standin.wav", "samples/music_bed.wav", -15); g2 = R.bed_gain("samples/voice_standin.wav", loud, -15)
    assert abs((g1 - g2) - 10) < 0.3

第 4 步:运行

$ bash make_music.sh
$ python remix.py samples/voice_standin.wav samples/music_bed.wav samples/final.wav
第一遍测量: {'input_i': '-18.16', 'input_tp': '-4.01', 'target_offset': '0.04'}
写入 samples/final.wav
$ python -c "import measure as M; r=M.measure('samples/final.wav'); print(r['lufs'], r['true_peak'])"
-15.9 -1.8
$ python -m pytest -q
4 passed

怎么检查结果?

  • 最终的 lufs 应该在目标 ±0.5 以内(测试 test_loudness_target)。
  • 戴耳机听:说话时音乐明显变小,开头和结尾的音乐能听清楚。
  • 释放时间是 400 ms,比句子之间的停顿(0.25~0.45 秒)长,所以短停顿时音乐不会完全回来。想让音乐回得更快,可以减小 release。

调试示例

1. 两遍 loudnorm 仍然没有达到目标。 第一版就用了两遍法,结果是 −16.4 LUFS。第一遍测得 input_i=-17.18、input_tp=-1.49:要达到 −16,需要再提高 1.2 dB,可真峰值已经是 −1.49,提高后会超过 −1.5 dBTP 的上限。这时 loudnorm 会为了守住峰值自动退回动态模式,响度也就达不到目标。

修复:在 loudnorm 之前加一个 alimiter(限幅到约 −4 dBFS),给线性增益留出余量。之后第一遍测得 input_tp=-4.01,最终 −15.9 LUFS / −1.8 dBTP。

2. 测试通过了,但音乐几乎听不见。 第一版用固定的 volume=-12dB 压低音乐,ducking 测试也通过了。但我打印出实际电平后发现,停顿时音乐只有 −51 dB。 原因是我合成的音乐本身只有约 −32 dB(ffmpeg 的 sine 信号源默认振幅是 1/8),再减 12 dB 就几乎听不见了。固定增益意味着结果取决于音乐文件本身的音量。

修复:分别测量人声和音乐的响度,计算增益,让音乐比人声低 --bed-db LU(默认 15)。修复后停顿时音乐约 −39 dB,说话时约 −54 dB。新增的测试确认:把音乐文件放大 10 dB 后,计算出的增益正好少 10 dB。

常见问题

现象 原因 处理
响度达不到目标 真峰值没有余量 保留 alimiter,或把目标调低一些
音乐太吵或太小 --bed-db 不合适 试试 −12 到 −20
音乐忽大忽小,像“抽气” ratio 太高或 release 太短 调小 ratio,或调大 release
最终文件比人声长 音乐比人声长 amix 已设置 duration=first,以人声长度为准

模板规则

  • 只增强人声,音乐在增强之后再混入。
  • 音乐音量相对人声设置,不用固定增益。
  • 用两遍 loudnorm,并留出峰值余量。
  • 背景音乐只用你有权使用的素材。

项目资源

用途 地址
Enhance Speech 常见问题 https://helpx.adobe.com/firefly/web/work-with-audio-and-video/work-with-audio/frequently-asked-questions-about-enhance-speech.html
Adobe Podcast 功能和套餐对比 https://podcast.adobe.com/en/features
Adobe Podcast 技术要求 https://helpx.adobe.com/podcast/technical-requirements.html
FFmpeg loudnorm 滤镜 https://ffmpeg.org/ffmpeg-filters.html#loudnorm
FFmpeg sidechaincompress 滤镜 https://ffmpeg.org/ffmpeg-filters.html#sidechaincompress

人声交给 Enhance Speech,音乐留在本地混:两者都清楚,响度也符合目标。