Enhance Speech 处理后“听起来更好”,但好多少?会不会削波?这篇做
measure.py: 对比处理前后的整体响度(LUFS)、真峰值、底噪、估算信噪比和削波采样数,并对容易误读的数据给出提示。
先看清成品
- 含噪录音和替身增强版对比:估算信噪比 19.3 → 27.4 dB(+8.1);相对底噪(floor_rel)−15.8 → −23.8(改善了 8 dB)。
- 发现了一个容易误读的数据:底噪的绝对值反而“升高”了 1.4 dB,因为整体增益提高了 9.4 LU;脚本会自动提示改看 floor_rel。
- 削波检测:把人声放大 30 dB,检测到 1000 个以上的削波采样。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 的输出。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
measure.py |
输出 JSON 对比报告 |
standin_enhance.sh |
本地替身处理链(不是 Adobe) |
test_measure.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) |
关键事实
| 事实 | 来源 |
|---|---|
| 音频格式 .mp3 .wav .m4a .flac .ogg .aac;视频 .mp4 .mov .m4v(视频和批量上传只限 Premium) | Enhance Speech FAQ |
| 免费版:单个文件最长 30 分钟、最大 500 MB,每天最多 1 小时 | FAQ、功能页 |
| Premium:单个文件最大 1 GB,每天最多 4 小时;功能页写的单个文件最长时长是 2 小时 | FAQ、功能页 |
| 下载的格式和上传的一样,不做转换 | FAQ、技术要求 |
| Enhance Speech 会过滤噪声和杂音、调整音高和音量,并做归一化 | FAQ |
EBU R128 响度和真峰值由 ffmpeg 的 ebur128=peak=true 滤镜计算 |
FFmpeg 滤镜文档 |
底噪 = 50 ms 帧 RMS 的第 10 百分位;语音电平 = 第 90 百分位;估算信噪比 = 两者之差。这是简单的客观估算,不是 Adobe 的指标。
文件结构
68-adobe-podcast-B-src/
├── measure.py
├── standin_enhance.sh
├── make_audio.sh
├── conftest.py
└── test_measure.py
第 1 步:生成测试音频(我自己合成的)
#!/usr/bin/env bash
# 生成测试用的“糟糕录音”(我自己合成的):espeak-ng 朗读 → 加粉红噪声、50 Hz 嗡声、回声。
set -euo pipefail
mkdir -p samples
espeak-ng -v en-us -s 150 -w samples/voice_clean.wav \
"Welcome back to the show. Today we are talking about home recording. First, pick a quiet room. Second, keep the microphone close. Third, record a few seconds of silence so you can hear the noise floor. That is all for today. Thanks for listening."
ffmpeg -loglevel error -y -i samples/voice_clean.wav \
-f lavfi -i "anoisesrc=color=pink:amplitude=0.04:sample_rate=22050" \
-f lavfi -i "sine=frequency=50:sample_rate=22050" \
-filter_complex "[0:a]aecho=0.8:0.6:60|120:0.35|0.2,apad=pad_dur=2[v];[2:a]volume=0.03[h];[v][1:a][h]amix=inputs=3:duration=first:normalize=0,aresample=48000" \
-ac 1 -c:a pcm_s16le samples/voice_noisy.wav
ffprobe -v error -show_entries format=duration,size -of compact samples/voice_noisy.wav
第 2 步:本地替身处理链
#!/usr/bin/env bash
# 本地替身(我自己的 ffmpeg 处理链,不是 Adobe Enhance Speech):去 50 Hz 嗡声、高通、FFT 降噪、响度归一。
# 只用来演示 measure.py 的对比流程。真实使用时请换成从 Adobe Podcast 下载的文件。
set -euo pipefail
ffmpeg -loglevel error -y -i "$1" -af "bandreject=f=50:width_type=h:w=10,highpass=f=80,afftdn=nf=-40:nr=24,loudnorm=I=-16:TP=-1.5:LRA=11" -ar 48000 "$2"
第 3 步:测量脚本
"""对比 Enhance Speech 处理前后的录音:响度(EBU R128 integrated LUFS)、真峰值、底噪、语音电平、估算信噪比、削波采样数。
数值用 ffmpeg 的 ebur128 滤镜和 numpy 计算,只用于客观对比;好不好听仍然要自己听。"""
import argparse, json, re, subprocess, sys
import numpy as np
def ebur128(path):
err = subprocess.run(["ffmpeg", "-hide_banner", "-nostats", "-i", path, "-af", "ebur128=peak=true", "-f", "null", "-"],
capture_output=True, text=True).stderr
summ = err[err.rfind("Summary:"):]
f = lambda k: float(re.search(k + r":\s+(-?[\d.]+|-inf)", summ).group(1).replace("-inf", "-inf"))
return {"lufs": f("I"), "lra": f("LRA"), "true_peak": f("Peak")}
def frames(path, sr=16000, ms=50):
raw = subprocess.run(["ffmpeg", "-loglevel", "error", "-i", path, "-ac", "1", "-ar", str(sr), "-f", "s16le", "-"], capture_output=True, check=True).stdout
x = np.frombuffer(raw, np.int16).astype(float) / 32768; n = sr * ms // 1000
return x, 20 * np.log10(np.sqrt((x[: len(x) // n * n].reshape(-1, n) ** 2).mean(1)) + 1e-9)
def measure(path):
x, db = frames(path); p10, p90 = np.percentile(db, [10, 90])
r = {"file": path, **ebur128(path), "noise_floor": round(float(p10), 1), "speech_level": round(float(p90), 1),
"snr_est": round(float(p90 - p10), 1), "clipped": int((np.abs(x) >= 0.999).sum())}
# 底噪相对整体响度:处理前后增益不同时,直接比较底噪的绝对值会误导
r["floor_rel"] = round(r["noise_floor"] - r["lufs"], 1)
return r
def compare(before, after):
a, b = measure(before), measure(after); notes = []
if abs(b["lufs"] - a["lufs"]) > 3:
notes.append(f"处理后整体响度变化了 {b['lufs'] - a['lufs']:+.1f} LU,底噪请看 floor_rel(相对响度),不要直接比较 noise_floor")
if b["snr_est"] - a["snr_est"] < 6: notes.append("估算信噪比提升不到 6 dB,效果可能不明显")
if b["true_peak"] > -1.0: notes.append(f"处理后真峰值 {b['true_peak']} dBTP,高于 -1 dBTP,转码后可能削波")
if b["clipped"] > a["clipped"]: notes.append("处理后削波采样增加")
return {"before": a, "after": b, "delta": {k: round(b[k] - a[k], 1) for k in ("lufs", "noise_floor", "floor_rel", "snr_est")}, "notes": notes}
if __name__ == "__main__":
ap = argparse.ArgumentParser(); ap.add_argument("before"); ap.add_argument("after"); a = ap.parse_args()
print(json.dumps(compare(a.before, a.after), ensure_ascii=False, indent=1))
第 4 步:测试
import subprocess, measure as M
def test_noisy_vs_clean():
c, n = M.measure("samples/voice_clean.wav"), M.measure("samples/voice_noisy.wav")
assert c["snr_est"] > n["snr_est"] + 20 and n["noise_floor"] > c["noise_floor"]
def test_standin_compare():
d = M.compare("samples/voice_noisy.wav", "samples/voice_standin.wav")
assert d["delta"]["snr_est"] > 6 and d["delta"]["floor_rel"] < -6
assert d["delta"]["noise_floor"] > 0 # 绝对底噪“变差”,只是因为整体增益提高了
assert any("floor_rel" in n for n in d["notes"])
def test_clipping_detected(tmp_path):
p = str(tmp_path / "clip.wav")
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-i", "samples/voice_clean.wav", "-af", "volume=30dB", p], check=True)
d = M.compare("samples/voice_clean.wav", p)
assert d["after"]["clipped"] > 1000 and any("削波" in n for n in d["notes"])
def test_same_file_no_gain_note():
d = M.compare("samples/voice_noisy.wav", "samples/voice_noisy.wav")
assert d["delta"]["lufs"] == 0 and any("不到 6 dB" in n for n in d["notes"])
第 5 步:运行
$ bash standin_enhance.sh samples/voice_noisy.wav samples/voice_standin.wav
$ python measure.py samples/voice_noisy.wav samples/voice_standin.wav # 实际输出是 JSON,这里把数值整理成三行
before: lufs -26.4 true_peak -5.9 noise_floor -42.2 speech_level -22.9 snr_est 19.3 floor_rel -15.8 clipped 0
after: lufs -17.0 true_peak -1.5 noise_floor -40.8 speech_level -13.4 snr_est 27.4 floor_rel -23.8 clipped 0
delta: lufs +9.4 noise_floor +1.4 floor_rel -8.0 snr_est +8.1
notes: ["处理后整体响度变化了 +9.4 LU,底噪请看 floor_rel(相对响度),不要直接比较 noise_floor"]
$ python -m pytest -q
4 passed
怎么检查结果?
- 换成从 Adobe Podcast 下载的真实文件:
python measure.py 原始.wav 增强后.wav。 - 关注
snr_est和floor_rel的变化,以及notes中的提示。 - 数据只是参考:最后仍然要戴耳机听一遍,注意有没有“机器人声”。
调试示例
底噪看起来变差了。 第一版只输出了 noise_floor 的绝对值:处理前 −42.2,处理后 −40.8,看起来底噪升高了 1.4 dB,好像越处理越吵。
但同时 lufs 从 −26.4 升到了 −17.0:处理链最后的 loudnorm 把整体音量提高了约 9.4 dB,底噪也跟着一起被放大了。把底噪换算成相对于整体响度的值(floor_rel = 底噪 − LUFS):−15.8 → −23.8,实际改善了 8 dB,和估算信噪比的 +8.1 dB 一致。
修复:加上 floor_rel 字段;整体响度变化超过 3 LU 时,自动提示不要直接比较 noise_floor。Enhance Speech 本身也会做归一化(官方 FAQ),所以对比真实文件时同样会遇到这个问题。
另一个观察: 替身处理链的 loudnorm 目标是 −16 LUFS,实际得到的是 −17.0。单遍 loudnorm 的结果并不精确,C 篇会处理这个问题。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| noise_floor 变高 | 整体增益提高了 | 看 floor_rel |
| snr_est 提升很小 | 原始录音本来就干净,或处理强度太低 | Premium 可以调节增强强度 |
| 真峰值高于 −1 dBTP | 转成有损格式后可能削波 | 做一次限幅,或降低整体音量 |
| lufs 显示 -inf | 文件是静音的 | 检查文件 |
模板规则
- 对比前后两个文件,不只看一个。
- 增益变化时看相对值。
- 数据和耳朵一起判断。
项目资源
| 用途 | 地址 |
|---|---|
| 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 ebur128 滤镜 | https://ffmpeg.org/ffmpeg-filters.html#ebur128 |
量化对比之后,你会知道 Enhance Speech 具体改善了多少,也会知道还有哪里需要调整。