双麦克风串声消除全攻略
摘要 本文系统探讨了双麦克风场景下的串声消除问题,针对两人面对面讲话时麦克风互相拾取对方语音的情况,提出了两种解决思路:语音分离(Speech Separation)和目标人声提取(Target Speech Extraction)。实验测试了TIGER、SpeechBrain和Asteroid等轻量级模型在不同场景下的表现,发现当说话人能量接近时分离效果较差,而能量差异较大时效果较好。文章总结了
串声如何消除
引言
写这篇博客的目的,是对双麦克风场景下的串声(cross-talk)问题进行一次系统性的记录与梳理。
本文讨论的具体场景为:两个人面对面讲话、每人各自使用一个近讲麦克风。在这种设置下,由于空间传播和指向性限制,不可避免地会出现一个麦克风同时拾取到本人与对方语音的情况,即串声问题。
在实际应用中,我们往往希望实现以下目标之一:
- 每个麦克风只保留对应说话人的语音;
- 或者通过后处理算法,尽可能抑制对方说话人的串声,得到相对干净的单人语音轨道。
思路初探
最初遇到这个问题时,我的第一反应是:语音分离(Speech Separation)。
这里的语音分离可以进一步细分为两类问题:
-
人声分离(Speech Separation / Blind Source Separation, BSS)
这类方法不依赖任何先验信息,只假设输入信号是多个声源的混合,目标是将其中的各个声源分离出来。
在工程上,可以理解为“不关心是谁在说话,只要把不同人的声音拆开即可”。 -
目标人声提取(Target Speech Extraction, TSE)
这类方法通常依赖目标说话人的先验信息(例如一段参考语音或声纹 embedding),在此基础上从混合语音中提取指定说话人的语音。
从建模角度看,可以理解为“声纹建模 + 条件人声分离”。
初步实验与模型尝试
受限于硬件条件(仅有 CPU、无 GPU),目前主要尝试了一些计算量相对较低的模型。
在语音分离方向上,尝试了清华大学近几年提出的一篇工作:TIGER: Time-frequency Interleaved Gain Extraction and Reconstruction for Efficient Speech Separation该模型的特点是计算效率较高、结构相对轻量,并且在真实场景下的双人语音分离任务中表现较为稳定。在有限算力条件下,其分离效果总体令人满意。
在目标人声提取(TSE)方向上,也简单尝试了一些已有模型。从初步结果来看,在说话人差异较明显、参考语音质量较好的情况下,提取效果是可接受的,但尚未进行系统性的评估和对比测试。
SpeechBrain 中的语音分离模型
SpeechBrain 提供了多种可直接用于推理的预训练语音分离模型,主要基于 SepFormer 及其变体,常见模型包括:
“sepformer-wsj02mix”: “speechbrain/sepformer-wsj02mix”,
“sepformer-wsj03mix”: “speechbrain/sepformer-wsj03mix”,
“sepformer-libri3mix”: “speechbrain/sepformer-libri3mix”,
“sepformer-libri2mix”: “speechbrain/sepformer-libri2mix”,
“sepformer-wham”: “speechbrain/sepformer-wham”,
“sepformer-whamr”: “speechbrain/sepformer-whamr”,
“sepformer-whamr16k”: “speechbrain/sepformer-whamr16k”,
“resepformer-wsj02mix”: “speechbrain/resepformer-wsj02mix”,
这些模型分别针对不同的数据集、说话人数以及噪声/混响条件进行训练,实际使用时需要根据应用场景进行合理选择。
一些实验观察与经验总结
在实际测试过程中,有以下几点比较明确的经验结论:
- 当两个说话人的能量水平接近时(例如距离麦克风相当、说话音量接近),
纯语音分离模型的效果通常会明显下降,残留串声较为严重。 - 当目标说话人与干扰说话人的能量差异较大时(例如一近一远、一大一小),
分离效果会显著改善,这类情况也是当前分离模型最擅长的场景。 - 模型与场景的匹配非常重要:
干净语音场景应优先选择在**干净数据集(如 WSJ0-2Mix)**上训练的模型;
若输入包含明显噪声或混响,却使用“干净模型”,反而会导致分离性能下降。
总体来看,SpeechBrain 提供的 SepFormer 系列模型在标准双人分离任务中具备较强的工程可用性,但在“双人近距离、能量接近、高度重叠”的串声场景下,仍然存在明显挑战,这也是后续需要进一步结合阵列信息、说话人先验或任务约束来解决的问题。
Asteroid 中的语音分离模型
项目地址
预训练模型地址,(噪声情况下),测试了一下,这个模型和我的场景不符合,这个是从多麦信号中,增强一个说话人的语音。我得到的结果也是,一个文件中的人声清晰(又噪声),另一个几乎不可听(单纯的质量差,不是没有声音)。
convtasnet,后面换了这个模型试试,还是又残留的声音,效果一般。
WSJ0-2Mix,8 kHz,经典基准。效果也可以,也有残余的声音。
import argparse
import json
import os
from pathlib import Path
from typing import Dict, Optional
import soundfile as sf
import torch
import torchaudio
from asteroid.models import BaseModel
from asteroid.utils.hub_utils import cached_download
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="使用 Asteroid 预训练模型进行推理。")
parser.add_argument(
"--model-source",
default=r"path\Asteroid\ConvTasNet_WHAM_sepclean\pytorch_model.bin",
help="本地模型文件路径/目录或 HF repo_id。",
)
parser.add_argument("--input", required=True, help="输入 wav 路径。")
parser.add_argument("--output-dir", default="outputs", help="输出目录。")
parser.add_argument(
"--online",
action="store_true",
help="允许在线拉取(默认离线模式)。",
)
parser.add_argument(
"--device",
default="cpu",
help="推理设备,例如 cpu 或 cuda。",
)
return parser.parse_args()
def _resample_audio(x: torch.Tensor, orig_sr: int, target_sr: int) -> torch.Tensor:
# 使用 torchaudio 进行重采样(更适合音频)
return torchaudio.functional.resample(x, orig_sr, target_sr)
def _resolve_model_source(model_source: str) -> str:
path = Path(model_source)
if path.exists() and path.is_dir():
# 兼容目录输入,优先查找常见权重文件名
for name in ("model.pt", "pytorch_model.bin"):
candidate = path / name
if candidate.exists():
return str(candidate)
raise FileNotFoundError("目录中未找到 model.pt 或 pytorch_model.bin")
return model_source
def _load_metadata(model_source: str) -> Optional[Dict]:
path = Path(model_source)
if path.exists() and path.is_file():
meta_path = path.parent / "metadata.json"
if meta_path.exists():
return json.loads(meta_path.read_text(encoding="utf-8"))
if path.exists() and path.is_dir():
meta_path = path / "metadata.json"
if meta_path.exists():
return json.loads(meta_path.read_text(encoding="utf-8"))
return None
def main() -> None:
args = _parse_args()
if not args.online:
os.environ["HF_HUB_OFFLINE"] = "1"
device = torch.device(args.device)
if device.type == "cuda" and not torch.cuda.is_available():
raise RuntimeError("请求使用 CUDA,但当前环境不可用。")
model_source = _resolve_model_source(args.model_source)
metadata = _load_metadata(model_source)
model_path = cached_download(model_source)
# 优先尝试 TorchScript,失败后再用 torch.load 加载序列化配置
try:
conf = torch.jit.load(model_path, map_location="cpu")
is_script_model = True
except Exception:
# torch>=2.6 默认启用 weights_only,需要显式关闭以加载完整配置
conf = torch.load(model_path, map_location="cpu", weights_only=False)
is_script_model = False
if is_script_model:
model = conf.to(device)
else:
model = BaseModel.from_pretrained(conf).to(device)
model.eval()
x, sr = sf.read(args.input)
target_sr = int(
(metadata or {}).get("sample_rate", getattr(model, "sample_rate", 16000))
or 16000
)
expected_multichannel = (metadata or {}).get("multichannel", None)
expected_in_channels = getattr(model, "in_channels", None)
if expected_multichannel is True or (
expected_multichannel is None and expected_in_channels and expected_in_channels > 1
):
if x.ndim == 1:
raise ValueError("该模型需要多通道输入 wav(至少 2 通道)。")
if expected_multichannel is False and x.ndim != 1:
raise ValueError("该模型需要单通道输入 wav。")
if sr != target_sr:
# 先转为 (channels, time) 进行重采样
x_t = torch.from_numpy(x).float()
if x.ndim == 1:
x_t = x_t.unsqueeze(0)
else:
x_t = x_t.transpose(0, 1)
x_t = _resample_audio(x_t, orig_sr=sr, target_sr=target_sr)
if x.ndim == 1:
x = x_t.squeeze(0).cpu().numpy()
else:
x = x_t.transpose(0, 1).cpu().numpy()
if is_script_model and x.ndim == 1:
x_tensor = torch.from_numpy(x).float().unsqueeze(0).to(device)
else:
# Asteroid 常用张量格式: (batch, channels, time)
if x.ndim == 1:
x = x[:, None]
x_tensor = torch.from_numpy(x).float().transpose(0, 1).unsqueeze(0).to(device)
with torch.no_grad():
if is_script_model:
est = model(x_tensor)
else:
est = model.separate(x_tensor)
if est.ndim == 1:
est = est.unsqueeze(0).unsqueeze(0)
if est.ndim == 2:
est = est.unsqueeze(0)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
n_src = est.shape[1]
for i in range(n_src):
wav = est[0, i].detach().cpu().numpy()
out_path = output_dir / f"spk{i + 1}.wav"
sf.write(out_path.as_posix(), wav, target_sr)
if __name__ == "__main__":
main()
Voice filter
论文地址.
voice-filter.
项目地址.
我跑出来的效果不好,就算是使用他仓库自带的音频,效果也不好。不是很清楚是什么情况。
Demucs 音乐源分离模型
根据其项目介绍,Demucs 是一款先进的音乐源分离模型,目前能够将鼓、贝斯和人声从其他伴奏中分离出来。
因此就没有测试了。
X-TF-GridNet
X-TF-GridNet:一种具有自适应说话人嵌入融合的时频域目标说话人提取网络
项目地址.
直接使用voice-filter给的音频来测试。
有的效果还可以,有的效果差。
Tiger
项目地址.
这个项目提出了一种显著降低参数规模和计算成本的语音分离模型:时频交错增益提取与重构网络(TIGER)
值得注意的是,TIGER 是首个参数量少于 100 万且性能接近最先进模型的语音分离模型。
其主要是对于真实世界的语音分离,对于合成的声音效果应该不会很好。
我测试了一下,使用其官方的gpu进行推理,效果还是不错的,但是使用cpu的时候明显要差一点。但是总体来说,还是比较好的。
感想
这些都是我测试过的,其实我发现效果就是speechbrain效果还不错。当然也不是说tse这些就不好,只是说可能不太适合?
后面想一想,串音吗,一个大声一个小声,能不能直接用门限方法给他去除呢。好像也是可以的。门限不仅可以降噪,还可以去除串音,这个在录制声音的时候应该也是有使用吧。就是防止录制到多个乐器。
ratio-based soft mask
Ratio-based soft mask 是一种较早提出、但在语音增强与语音分离中仍被广泛采用的时频域方法,其核心思想是在每一个时频单元上,根据不同声源的相对能量占优关系构建连续取值的掩膜函数,从而实现对目标声源的保留与对干扰声源的抑制。
场景说明
在当前场景中,存在两路音轨:(双人场景)
- 音轨 x 1 ( t ) x_1(t) x1(t):主说话人为A,其他说话人都归为B,能量较小。
- 音轨 x 2 ( t ) x_2(t) x2(t):主说话人为B,其他说话人都归为A,能量较小。
两路音轨都是进场的,即各自的音轨中,具有非常明显的能量优势。
对两个信号进行短时傅里叶变换,得到时频表示为:
X 1 ( t , f ) = S T F T ( x 1 ( t ) ) X_1(t,f)=STFT(x_1(t)) X1(t,f)=STFT(x1(t))
X 2 ( t , f ) = S T F T ( x 2 ( t ) ) X_2(t,f)=STFT(x_2(t)) X2(t,f)=STFT(x2(t))
其实可以讲其中一个当作说话人,另一个当作是噪声就可以了,这样就可以回归到IAM这种掩膜方法了,便于理解。
此外这里不对相位做额外处理。
掩膜构建
然后再每一个 ( t , f ) (t,f) (t,f)单元上构建掩膜就可以。
M 1 ( t , f ) = ∣ X 1 ( t , f ) ∣ p ∣ X 1 ( t , f ) ∣ p + ∣ X 2 ( t , f ) ∣ p + δ M_1(t,f)=\frac{|X_1(t,f)|^p}{|X_1(t,f)|^p+|X_2(t,f)|^p+\delta} M1(t,f)=∣X1(t,f)∣p+∣X2(t,f)∣p+δ∣X1(t,f)∣p
M 2 ( t , f ) = ∣ X 2 ( t , f ) ∣ p ∣ X 1 ( t , f ) ∣ p + ∣ X 2 ( t , f ) ∣ p + δ M_2(t,f)=\frac{|X_2(t,f)|^p}{|X_1(t,f)|^p+|X_2(t,f)|^p+\delta} M2(t,f)=∣X1(t,f)∣p+∣X2(t,f)∣p+δ∣X2(t,f)∣p
其中 p ≥ 1 p≥1 p≥1
- p = 1 : p=1: p=1:较软的比例掩膜
- p = 2 : p=2: p=2:Wiener-like 掩膜,抑制能力更强
δ : \delta: δ:数值稳定项,防止分母为零
在理想情况下,上述掩膜满足近似互补关系:
M 1 ( t , f ) + M 2 ( t , f ) ≈ 1 M_1(t,f)+M_2(t,f)≈1 M1(t,f)+M2(t,f)≈1
即在每个时频单元中,两路信号对能量的“占比”之和接近 1。
使用
直接讲构建好的掩膜用在对应的复数谱上即可:
随后通过逆短时傅里叶变换(iSTFT)回到时域,得到抑制串音后的信号。
虽然,感觉声音去除了,但是感觉声音变闷了,音质有一定的影响。
总结
总体来说,针对这个场景,如果不是对音质要求特别高的话,用传统的也可以。对音质要求高的话,可以考虑用tiger然后加一个降噪应该是没问题的,当然是后处理了,需要gpu,我感觉cpu效果一般。
更多推荐


所有评论(0)