效率工具

Adobe Podcast:Enhance Speech 处理后“听起来更好”,但好多少

Enhance Speech 处理后“听起来更好”,但好多少?会不会削波?这篇做 measure.py: 对比处理前后的整体响度(LUFS)、真峰值、底噪、估算信噪比和削波采样数,并对容易误读的数据给出提示。

BINBHO · 图文教程

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 具体改善了多少,也会知道还有哪里需要调整。