借助深度学习模型实现分离人声与背景声
Published in:2025-01-01 |
Words: 3.5k | Reading time: 16min | reading:

借助深度学习模型实现分离人声与背景声

spleeter

Spleeter 是一个由 Deezer 开发的音频源分离工具,支持将音频分离为伴奏和人声。

技术库

  • spleeter
  • ffmpeg
  • open-unmix
  • demuicx

安装使用

1
pip install spleeter

简单demo

1
spleeter separate -i audio.wav -p spleeter:2stems -o output/

实现人声与背景音分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import os
import subprocess
from spleeter.separator import Separator
import shutil

# 定义输入和输出路径
video_file = 'input_video.mp4' # 输入视频文件
audio_file = 'extracted_audio.wav' # 提取的音频文件
output_dir = 'output' # 输出文件夹

# 步骤 1: 使用 ffmpeg 提取音频
def extract_audio_from_video(video_file, audio_file):
command = [
'ffmpeg', '-i', video_file, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2', audio_file
]
subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Audio extracted to {audio_file}")

# 步骤 2: 使用 Spleeter 分离音频中的人声和伴奏
def separate_audio(audio_file, output_dir):
separator = Separator('spleeter:2stems') # 选择分离人声和伴奏
separator.separate_to_file(audio_file, output_dir)
print(f"Audio separation complete. Files saved in {output_dir}")

# 步骤 3: 合并去除人声的伴奏与原视频
def merge_audio_with_video(video_file, audio_file, output_video_file):
command = [
'ffmpeg', '-i', video_file, '-i', audio_file, '-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental', output_video_file
]
subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Video saved as {output_video_file}")

# 主程序
def process_video(video_file):
# 提取音频
extract_audio_from_video(video_file, audio_file)

# 分离音频
separate_audio(audio_file, output_dir)

# 获取分离后的伴奏音频路径
accompaniment_audio = os.path.join(output_dir, os.path.basename(audio_file).replace('.wav', '/accompaniment.wav'))

if os.path.exists(accompaniment_audio):
# 合并伴奏和视频
output_video_file = 'output_video.mp4'
merge_audio_with_video(video_file, accompaniment_audio, output_video_file)
print("Process complete, output video created.")
else:
print("Error: Accompaniment audio file not found.")

# 执行
process_video(video_file)

ffmpeg 提取音频时,默认将视频中的音频轨道提取为 .wav 格式。如果你的音频有不同的格式(如 .mp3),你可以根据需要修改提取音频的格式。

Spleeter 使用的是深度学习模型,因此分离质量取决于模型的效果,尽管它能较好地分离人声和伴奏,但结果可能不是完美的。

输出文件说明

  • accompaniment.wav:分离出的伴奏音轨,去除了人声。

  • vocals.wav:分离出的人声音轨。

  • output_video.mp4:生成的去人声视频。

GUI 实现

  • 分割视频出音频数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from moviepy.editor import VideoFileClip

def extract_audio_from_video(video_path, output_audio_path):
"""
从视频中提取音频并保存为 WAV 格式。

:param video_path: 输入视频文件路径
:param output_audio_path: 输出音频文件路径 (例如: 'output_audio.wav')
"""
try:
# 读取视频文件
video = VideoFileClip(video_path)

# 提取音频
audio = video.audio

# 写入音频文件,保存为 WAV 格式
audio.write_audiofile(output_audio_path, codec='pcm_s16le')

# 释放资源
audio.close()
video.close()

print(f"音频已成功提取并保存为: {output_audio_path}")

except Exception as e:
print(f"处理视频时出错: {e}")

# 示例使用
video_path = "input_video.mp4" # 输入的视频文件路径
output_audio_path = "output_audio.wav" # 输出的音频文件路径

extract_audio_from_video(video_path, output_audio_path)

在如下路径下载(需要科学上网)

1
https://makenweb.com/SpleeterGUI

下载成功后,导入上述步骤输出音频输出开始转换

demucs 分离

Demucs 是 Facebook AI Research (FAIR) 发布的一个强大的深度学习音频分离模型,它能够将音乐分离成多个成分(如人声、鼓声、贝斯、其他)。Demucs 采用了时域卷积网络(Temporal Convolutional Networks,TCNs),在音频分离中表现得非常优秀,特别是在复杂音频和多种音频成分混合的情况下

安装使用

1
2
pip install demucs
demucs output.wav

openunmix 分离

Open-Unmix 是一个专为音频分离设计的开源深度学习模型,特别适用于分离音乐中的人声与伴奏。它是基于 PyTorch 实现的,能够将音频分离成多个来源(通常为人声、鼓、贝斯和其他伴奏)。

1
2
pip install openunmix
umx input_audio.wav

脚本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# openumix
import openunmix

# 分离音频
input_file = "input_audio.wav"
output_dir = "output_directory"
umx = openunmix.Umx()
umx.separate(input_file, output_dir)
# demucs
import demucs

# 分离音频
input_file = "input_audio.wav"
output_dir = "output_directory"
demucs.separate(input_file, output_dir)
  • 无音频数据视频与无人声数据合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import os
import subprocess


def merge_video_audio(video_file, audio_file, output_file):
"""
使用 FFmpeg 合并视频和音频。

:param video_file: 输入的视频文件路径
:param audio_file: 输入的音频文件路径
:param output_file: 合并后的输出视频路径
"""
command = [
r'D:\ffmpeg-7.0.2-essentials_build\bin\ffmpeg.exe', # FFmpeg 可执行文件的路径
'-i', video_file, # 输入视频文件
'-i', audio_file, # 输入音频文件
'-c:v', 'copy', # 保持视频编解码格式不变
'-c:a', 'aac', # 设置音频编码格式为 AAC
'-strict', 'experimental', # 允许使用实验性的音频编解码器
'-map', '0:v:0', # 映射视频流
'-map', '1:a:0', # 映射音频流
'-y', # 覆盖输出文件(如果存在)
output_file # 输出合并后的文件
]

try:
subprocess.run(command, check=True)
print(f"视频和音频已成功合并并保存为: {output_file}")
except subprocess.CalledProcessError as e:
print(f"合并过程中出现错误: {e}")


def scan_and_merge(video_dir, audio_dir, output_dir):
"""
扫描视频和音频文件夹中的所有文件,并合并一一对应的文件。

:param video_dir: 存放视频文件的目录
:param audio_dir: 存放音频文件的目录
:param output_dir: 输出合并后文件的目录
"""
# 递归获取视频文件夹中的所有 .mp4 文件
video_files = []
for root, dirs, files in os.walk(video_dir):
for file in files:
if file.endswith('.mp4'):
video_files.append(os.path.join(root, file))

# 递归获取音频文件夹中的所有 .wav 文件
audio_files = []
for root, dirs, files in os.walk(audio_dir):
for file in files:
if file.endswith('.wav'):
audio_files.append(os.path.join(root, file))

# 确保输出文件夹存在
if not os.path.exists(output_dir):
os.makedirs(output_dir)

for video_file in video_files:
# 获取不带路径的文件名(不包括扩展名)
video_name = os.path.splitext(os.path.basename(video_file))[0].replace("_no_audio",'')

# 查找对应的音频文件(从音频文件所在的父目录名称进行匹配)
matching_audio_file = None
for audio_file in audio_files:
# 获取音频文件的父目录名称
audio_file_parent_dir = os.path.basename(os.path.dirname(audio_file))

# 使用音频文件父目录名称进行匹配
if video_name in audio_file_parent_dir:
matching_audio_file = audio_file
break

if matching_audio_file:
# 构造输出文件路径
output_path = os.path.join(output_dir, f"{video_name}_merged.mp4")

# 合并视频和音频
merge_video_audio(video_file, matching_audio_file, output_path)
else:
print(f"找不到与 {video_file} 对应的音频文件.")


# 示例使用
if __name__ == "__main__":
video_dir = r'D:\pythonProject\audio_record_server\src\utils\output_video_dir' # 视频文件夹路径
audio_dir = r'D:\pythonProject\audio_record_server\src\utils\output_audio_umxl' # 音频文件夹路径
output_dir = r'D:\pythonProject\audio_record_server\src\utils\output_result' # 输出文件夹路径

scan_and_merge(video_dir, audio_dir, output_dir)

效果对比

spleeter demuics open-unmix
人声分离效果 较好 较好
有无GUI
速度 10s 5s 2s
备注

上述对比为同时处理32s时长 wav格式采样率为44100khz,比特率:1411kps,立体声的音频结果对比

效果测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# _*_ coding: utf-8 _*_
"""
Time: 2024/12/26 16:57
Author: ZhaoQi Cao(czq)
Version: V 0.1
File: video_split_audio.py
Describe: Write during the python at zgxmt, Github link: https://github.com/caozhaoqi
"""
import os
import subprocess
import time
import logging
import threading
from concurrent.futures import ThreadPoolExecutor
import openunmix
import demucs
from moviepy import VideoFileClip, AudioFileClip, CompositeVideoClip
from tqdm import tqdm
import soundfile as sf
import numpy as np

from utils.meger_video_audio import scan_and_merge

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# 全局变量
OUTPUT_AUDIO_UMXL = "./output_audio_umxl"
OUTPUT_AUDIO_DEMUCS = "./output_audio_demucs"
PROCESSED_FILES = set() # 用于记录已处理的文件


def extract_audio_from_video(video_path, output_audio_dir, output_video_dir):
"""
从视频中提取音频并保存为 WAV 格式。

:param video_path: 输入视频文件路径
:param output_audio_dir: 输出音频文件目录
"""
try:
# 获取视频文件名(不含扩展名)
video_name = os.path.basename(video_path).split(".")[0]

# 输出音频文件路径
output_audio_path = os.path.join(output_audio_dir, f"{video_name}.wav")

# 输出不含音频的视频文件路径
output_video_path = os.path.join(output_video_dir, f"{video_name}_no_audio.mp4")

# 检查音频文件是否已经存在,避免重复提取
if os.path.exists(output_audio_path):
logging.warning(f"音频文件已存在,跳过提取:{output_audio_path}")
else:
logging.info(f"开始提取音频:{video_path}")
# 读取视频文件
video = VideoFileClip(video_path)
# 提取视频中的音频
audio = video.audio
# 保存音频为 WAV 格式
audio.write_audiofile(output_audio_path, codec='pcm_s16le')
# 释放音频资源
audio.close()

# 读取视频文件
video = VideoFileClip(video_path)
# 将视频的音频部分设为 None,从而去除音频
video_without_audio = video.without_audio()

# 保存不含音频的视频
video_without_audio.write_videofile(output_video_path, codec="libx264", audio=False)

# 释放视频资源
video.close()
video_without_audio.close()

logging.info(f"音频已成功提取并保存为: {output_audio_path}")
logging.info(f"不含音频的视频已保存为: {output_video_path}")

return output_audio_path, output_video_path

except Exception as e:
logging.error(f"处理视频时出错: {e}")
return None, None


def process_audio_umx(file_path, output_dir):
"""
使用 UMX 模型进行音频分离,并添加源文件名到输出文件名中
"""
try:
if file_path in PROCESSED_FILES:
logging.warning(f"文件已处理,跳过 UMX: {file_path}")
return
logging.info(f"开始 UMX 处理: {file_path}")
# 使用 openunmix.umx.separate 函数调用
# 从文件路径中提取文件名(不包含扩展名)
file_name = os.path.basename(file_path).split(".")[0]
output_prefix_dir = os.path.join(output_dir, file_name) # 添加前缀到输出目录
os.makedirs(output_prefix_dir, exist_ok=True)
# if umx_args is None:
# umx_args = []
# # 构建完整的 umx 命令
command = ["umx", file_path, "--outdir", output_prefix_dir]

result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
logging.error(f"UMX 执行出错: {result.stderr}")
return

logging.info(f"UMX 处理完成: {file_path}")
PROCESSED_FILES.add(file_path) # 记录已处理
except Exception as e:
logging.error(f"处理UMX时出错: {e}")


def process_audio_demucs(file_path, output_dir):
"""
使用 Demucs 模型进行音频分离,并添加源文件名到输出文件名中
"""
try:
if file_path in PROCESSED_FILES:
logging.warning(f"文件已处理,跳过 Demucs: {file_path}")
return
logging.info(f"开始 Demucs 处理: {file_path}")
# 使用 demucs.api.separate_audio 函数调用
# 从文件路径中提取文件名(不包含扩展名)
file_name = os.path.basename(file_path).split(".")[0]
output_prefix_dir = os.path.join(output_dir, file_name) # 添加前缀到输出目录
os.makedirs(output_prefix_dir, exist_ok=True) # 创建输出目录

# if umx_args is None:
# umx_args = []
# 构建完整的 umx 命令
command = ["umx", file_path, "--outdir", output_prefix_dir]

result = subprocess.run(command, capture_output=True, text=True, encoding='utf-8') # 指定编码
if result.returncode != 0:
logging.error(f"UMX 执行出错: {result.stderr}")
return

logging.info(f"UMX 处理完成: {file_path}")
PROCESSED_FILES.add(file_path) # 记录已处理
except Exception as e:
logging.error(f"处理UMX时出错: {e}")


def process_all_audio_files(audio_files, output_umx_dir, output_demucs_dir):
"""
多线程处理所有音频文件
:param audio_files:
:return:
"""
logging.info(f"开始处理 {len(audio_files)} 个音频文件.")
with ThreadPoolExecutor(max_workers=4) as executor:
futures = []
for file_path in audio_files:
futures.append(executor.submit(process_audio_umx, file_path, output_umx_dir))
# futures.append(executor.submit(process_audio_demucs, file_path, output_demucs_dir))
for future in tqdm(futures, desc="处理进度"):
future.result() # 等待完成并处理异常
logging.info("所有音频文件处理完成.")


def collect_audio_from_dir(input_dir, output_audio_dir, output_video_dir):
"""
读取指定目录下的所有视频文件,提取音频
:param input_dir:
:return: 所有音频文件列表
"""
audio_files = []
video_files = [os.path.join(input_dir, filename)
for filename in os.listdir(input_dir)
if filename.lower().endswith(('.mp4', '.mov', '.mkv', '.avi', '.webm'))]
with tqdm(total=len(video_files), desc="提取音频", unit="file") as pbar:
for video_path in video_files:
audio_file, video_files_out = extract_audio_from_video(video_path, output_audio_dir, output_video_dir)
if audio_file:
audio_files.append(audio_file)
pbar.update(1)
return audio_files


def get_all_files_from_dir(dir):
"""
从指定目录读取所有文件
"""
all_files = []
for root, dirs, files in os.walk(dir):
for file in files:
file_path = os.path.join(root, file)
all_files.append(file_path)
return all_files


if __name__ == "__main__":
input_video_dir = r'E:\download\youtobe_video\test' # 输入视频文件目录
output_audio_dir = "output_audio" # 输出音频目录
output_umx_dir = "output_audio_umxl"
output_demucs_dir = "output_audio_demucs"
output_video_dir = "output_video_dir"

# 创建输出目录(如果不存在)
os.makedirs(output_audio_dir, exist_ok=True)
os.makedirs(output_umx_dir, exist_ok=True)
os.makedirs(output_demucs_dir, exist_ok=True)
os.makedirs(output_video_dir, exist_ok=True)

logging.info("开始提取音频 单独保存无音频视频文件")
# 1. 从指定目录读取视频文件并提取音频
audio_files = collect_audio_from_dir(input_video_dir, output_audio_dir, output_video_dir)

# 2. 多线程处理所有音频文件
if audio_files:
logging.info("开始处理音频文件 输出无人声音频")
process_all_audio_files(audio_files, output_umx_dir, output_demucs_dir)
else:
logging.warning("没有找到任何音频文件,请检查输入目录")

# 3. 读取所有输出文件
# logging.info("输出文件")
# all_output_files_umx = get_all_files_from_dir(output_umx_dir)
# all_output_files_demucs = get_all_files_from_dir(output_demucs_dir)
logging.info("合并音(无人声)视频(无音频)文件 输出无人声视频文件")
video_dir = r'D:\pythonProject\audio_record_server\src\utils\output_video_dir' # 视频文件夹路径
audio_dir = r'D:\pythonProject\audio_record_server\src\utils\output_audio_umxl' # 音频文件夹路径
output_dir = r'D:\pythonProject\audio_record_server\src\utils\output_result' # 输出文件夹路径

scan_and_merge(video_dir, audio_dir, output_dir)
logging.info("处理完成")

全流程耗时统计

项目 时间 备注
分离视频音频 <1s 使用时长为10分15秒时长视频测试
音频去除人声(umx) 40s
音频去除人声(demucs) 36s
视频分离音频 145s/141s CPU占用极大
合并音视频 2s

one more thing

  • 噪声去除

使用pydub和noisereduce库去除,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# _*_ coding: utf-8 _*_
"""
Time: 2024/12/27 11:40
Author: ZhaoQi Cao(czq)
Version: V 0.1
File: noise_split_audio.py
Describe: Write during the python at zgxmt, Github link: https://github.com/caozhaoqi
"""
import noisereduce as nr
import numpy as np
import pydub
from pydub import AudioSegment


def reduce_noise(input_audio_path, output_audio_path):
# 加载音频文件
audio = AudioSegment.from_wav(input_audio_path)

# 转换音频为 numpy 数组格式(`pydub` 默认以 16-bit PCM 格式加载)
samples = audio.get_array_of_samples()
samples = np.array(samples)

# 使用 noisereduce 去除噪声
reduced_noise_samples = nr.reduce_noise(y=samples, sr=audio.frame_rate)

# 转换去噪后的样本回 `pydub` 格式
reduced_audio = AudioSegment(
reduced_noise_samples.tobytes(),
frame_rate=audio.frame_rate,
sample_width=audio.sample_width,
channels=audio.channels
)

# 保存去噪后的音频
reduced_audio.export(output_audio_path, format="wav")
print(f"去噪后的音频已保存为: {output_audio_path}")


# 示例使用
input_audio = r"D:\pythonProject\audio_record_server\src\utils\output_audio\比赛集锦:Highlights——Ma Jinbao VS Kanoya Ryohei (2018 LA Open).wav" # 输入音频文件
output_audio = "output_audio_reduced.wav" # 输出去噪音频文件

reduce_noise(input_audio, output_audio)

See

about me 个人微信

img

wechat offical 微信公众号

img

Prev:
在ubuntu系统使用docker安装 CVAT
Next:
label studio引入nemo_asr实现预测标注文本