简介:一款基于C#开发的音源分离工具源码,面向音乐制作人、音频工程师和音视频后期爱好者。工具依托深度学习算法,可将任意歌曲或音频文件拆分为鼓点、贝斯、人声等独立轨道,既可辅助重新混音,也能去除录音中的鸟鸣、路声、喇叭声等背景噪声。压缩包共一百一十五个文件,整体仅一点九五兆字节,包含C#源码、XML配置、资源文件、动态链接库、可执行程序以及解决方案工程等类型,打开工程即可查看界面布局、音频处理与模型调用相关代码。已有六十人学习下载。阅读源码能够完整梳理音频轨道分离流程、模型加载与推理方式、结果导出策略,以及图形界面事件处理等核心实现,适合希望将音源分离能力集成到自有工具,或学习C#桌面应用架构的开发者参考。该工具还提供简洁直观的操作界面,让初学者也能快速上手。程序运行需另配约一点二吉字节的完整模型文件,这是执行音源分离所依赖的必要外部条件。
1. SpleeterGUI 音源分离工具:把 Python 的音源分离能力装进 C# 桌面壳
做音乐后期的人常遇到这种场景:拿到一段带鸟鸣、路声的现场录音,想把干净人声抠出来重新混音;或者想扒一首歌的分轨,提取鼓点和贝斯。SpleeterGUI 就是干这事的——用 C# 写的桌面工具,底层调用 Deezer 开源的 Spleeter 音源分离引擎,把歌曲拆成鼓点、贝斯、人声等独立轨道。它解决的痛点很直接:原版是 Python 命令行,要装依赖、记参数;C# 打包后双击就能用,也便于集成进自己的工具链。适合音频后期、音乐制作人,以及想参考 C# 调用 Python 推理方案的程序员。注意这里的“分离”不是简单滤波,而是模型对频谱的掩码重构,理解这点对后面调参很关键。
2. 分离原理与模型选型:2-stem、4-stem、5-stem 该选哪个
2.1 Spleeter 的 U-Net 分离原理:为什么它不是普通滤波器
Spleeter 的核心是一个 U-Net 结构的卷积神经网络,输入是音频的短时傅里叶变换(STFT)幅度谱,输出是对每个声源的掩码。处理时先对音频加窗分帧做 STFT,得到二维的时频图;网络预测每个时频单元的“属于某个声源的概率”,然后把原频谱乘上这个掩码,再通过逆 STFT 重构出该声源的波形。因为掩码是学习出来的,所以它能把同频段重叠的音色分开,这一点传统高通、低通、噪声门都做不到。鸟鸣和人声都在中高频,滤波器一刀切会把两者都砍掉,而 Spleeter 能靠上下文的声学特征把它们掰开——当然,掰得干不干净,取决于训练数据和模型复杂度。
理解 Spleeter 的瓶颈也很重要。它训练的素材以 44.1kHz 采样率的主流流行音乐为主,模型对非音乐的语音、纯环境声并不擅长。比如你丢一段演讲录音进去想“去鸟鸣”,模型会把人声以外的一切都归到伴奏轨道,鸟鸣、路声、喇叭声全黏在一起。我一开始在 SpleeterGUI 里去录音里的喇叭声,结果喇叭声确实从人声里消失了,但和贝斯、鼓声混在同一轨,并没有“干净去除”。这个认知要放在前面,否则期望会落空。
模型训练数据决定了它的“音源”概念。Spleeter 能较好分离的声源是鼓、贝斯、人声、钢琴、其他乐器组。如果你输入一段只有一个人讲话的录音,它依然会尝试输出一个“伴奏”轨道,里面全是环境噪声和语音残留。所以在使用 SpleeterGUI 之前,先判断你要处理的东西是不是音乐或带音乐伴奏的录音;如果是纯语音降噪,建议去找专门的语音增强模型,而不是用音源分离工具硬扛。
2.2 模型选型:2-stems、4-stems 还是 5-stems
Spleeter 官方提供了 2-stems(人声/伴奏)、4-stems(鼓点/贝斯/人声/其他)、5-stems(鼓点/贝斯/人声/钢琴/其他)三种预训练模型。选型不复杂,核心看你要拿分离结果做什么。下面这个表是我在 GUI 里直接作为“帮助文档”展示给用户的:
| 模型 | 输出轨道 | 典型用途 | 处理速度 |
|---|---|---|---|
| 2-stems | vocals, accompaniment | 去人声做伴奏、提取人声 | 最快,内存占用低 |
| 4-stems | drums, bass, vocals, other | 重新混音、扒带、去环境声 | 中等 |
| 5-stems | drums, bass, vocals, piano, other | 需要钢琴轨的翻奏、混音 | 最慢,内存占用高 |
我的经验:如果只是做卡拉 OK 伴奏,用 2-stems;如果做 remix 或需要单独调鼓点和贝斯,用 4-stems;钢琴是刚需才上 5-stems。注意 5-stems 模型文件比 4-stems 大不少,首次运行要下载权重,网络不好时可能卡很久。SpleeterGUI 里最好把模型下载做成手动触发的,并且在界面显示下载进度,否则用户以为程序卡死了。这是 C# 开发里最常见的“黑匣子”问题——子进程在跑,UI 不知道,就会表现成假死。
还有一个容易忽略的点:stems 数量影响输出轨道的命名。2-stems 输出vocals.wav和accompaniment.wav;4-stems 输出vocals.wav、drums.wav、bass.wav、other.wav;5-stems 多一个piano.wav。如果你的后续脚本依赖文件名,这部分是硬编码的,升级模型后要检查一致性。
在 C# 源码里,模型选择通常只是一个枚举到字符串的映射。我一般会写一个静态方法,供 UI 和进程封装层共用:
public static string GetSpleeterModel(int stems) => stems switch { 2 => "spleeter:2stems", 4 => "spleeter:4stems", 5 => "spleeter:5stems", _ => throw new ArgumentOutOfRangeException(nameof(stems)) };这个方法把下拉框里的数字变成 Spleeter CLI 接受的模型名。注意spleeter:2stems里的冒号是必需的,少一个字符都会让 Separator 报“model not found”。我在给别人 review 代码时见过有人把冒号替换成下划线,导致一启动就崩——这类低级错误最好用单元测试拦住。
2.3 音频格式、采样率与输出参数:分离前先做归一化
Spleeter 官方要求输入音频被重采样到 44100 Hz,输出默认是 WAV 格式,16-bit PCM。GUI 上常见可调参数有三个:stems 数、输出格式、输出码率。输出格式支持 WAV 或 MP3,如果选 MP3,码率参数才有效,常见 192k 或 320k。我看到不少新手直接拿微信语音的 32kHz 音频去分离,出来的结果高频毛刺严重,这不是模型问题,是采样率没对齐。
SpleeterGUI 里最好在调用前自动重采样:先用 NAudio 读取音频,如果采样率不是 44100,就先转格式,再丢给 Python 进程。这个预处理比调任何参数都重要。常见的转换命令是用 ffmpeg 兜底,因为 C# 侧用 NAudio 解码 MP3 可以,但遇到 HE-AAC、Opus 这些格式还是会翻车。我在 GUI 里会封装一个方法,把任意输入先转成临时 WAV:
ffmpeg -i "input.mkv" -vn -ar 44100 -ac 2 -sample_fmt s16 "temp.wav"参数说明:-vn表示忽略视频轨,防止带画面的 MV 把视频流也读进来;-ar 44100强制重采样率;-ac 2转成双声道;-sample_fmt s16输出 16-bit PCM。转换后的临时 WAV 放在系统临时目录,分离完成后再删除。这样即使原文件是 48kHz 的 FLAC 或者 22.05kHz 的语音录音,也能保证 Spleeter 拿到它最熟悉的输入格式。
位深方面,建议输出用 16-bit 或 24-bit。16-bit 兼容性好,24-bit 动态范围更宽,适合后续进 DAW 做混音。如果你的拖入文件是 MP3 或 AAC,需要先解码成 PCM,这一步 ffmpeg 也会自动完成。还有一点,Spleeter 引擎内部固定用 44100 Hz 的傅里叶变换窗口,所以输出文件的采样率也是 44100 Hz。如果你希望最终工程是 48kHz,分离后还要在 DAW 或转码工具里再重采样一次,别在 GUI 里加一个“输出采样率”选项——那是无效参数,我最初加过,后来发现底层根本不会变,干脆去掉,免得用户以为模型坏了。
3. 拆解 SpleeterGUI 的 C# 源码:调用链、参数传递与进度回调
3.1 源码工程结构:界面、进程封装、Python 脚本怎么分工
一个能跑的 SpleeterGUI 不会把所有逻辑塞进 WinForms 的按钮事件里。我见过最省心的工程分四层:UI 层(MainForm.cs)、进程封装层(SpleeterProcess.cs)、配置层(AppConfig.cs)、外部脚本(spleeter_run.py)。目录大致这样:
SpleeterGUI/ ├── SpleeterGUI.sln ├── SpleeterGUI/ │ ├── MainForm.cs │ ├── SpleeterProcess.cs │ ├── AppConfig.cs │ └── bin/Release/ ├── python/ │ └── spleeter_run.py └── models/ # 首次运行时由 spleeter 自动下载MainForm 只负责收集用户输入:拖入的音频路径、stems 下拉框、输出目录、GPU 开关。点“开始分离”后,它把参数塞进一个AppConfig对象,然后调用SpleeterProcess.Start(config),进程封装层再启动 Python。这样 UI 线程不会被阻塞,分离过程中用户可以取消或同时处理下一首。这个分层是 C# 上位机开发里的老套路:界面归界面,业务归业务,进程归进程。别在按钮点击事件里WaitForExit,那是典型的入门翻车点,界面直接卡死,连取消都点不了。
Python 脚本那边不要写太复杂,它只做三件事:解析参数、调用spleeter.separate、把异常以 UTF-8 打到 stderr。核心代码很短,但要注意 Windows 下 Python 默认编码,我一般会在脚本第一行加# -*- coding: utf-8 -*-,并且在重定向输出时显式指定编码,否则 C# 读到的中文路径会变成乱码。
# spleeter_run.py import argparse import sys from spleeter.separator import Separator def main(): parser = argparse.ArgumentParser() parser.add_argument("-i", "--input", required=True, help="input audio path") parser.add_argument("-o", "--output", required=True, help="output directory") parser.add_argument("-m", "--model", default="spleeter:4stems", help="model name") args = parser.parse_args() separator = Separator(args.model, stft_backend="librosa") separator.separate_to_file(args.input, args.output) return 0 if __name__ == "__main__": sys.exit(main())这个脚本不输出百分比,但 Python 的异常堆栈会走 stderr,被 C# 重定向读到。stft_backend可以设为tensorflow或librosa,某些环境用librosa能绕开 TensorFlow 算子的兼容问题。注意separate_to_file的第二个参数是输出目录,不是文件路径;它会在目录下再建一个以模型名命名的子文件夹。我在 C# 侧处理输出文件时会等这个子目录出现,而不是等固定的文件路径,否则可能拿到空结果。
3.2 C# 启动 Python 子进程:参数用引号包住,输出用异步读
最核心的一段代码在SpleeterProcess.cs,我习惯写成这样:
public async Task<int> StartAsync(AppConfig cfg, CancellationToken token) { var psi = new ProcessStartInfo { FileName = cfg.PythonPath, Arguments = BuildArguments(cfg), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var proc = new Process { StartInfo = psi }; proc.OutputDataReceived += (_, e) => OnOutput(e.Data); proc.ErrorDataReceived += (_, e) => OnError(e.Data); proc.Start(); proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); await proc.WaitForExitAsync(token); return proc.ExitCode; }这里的三个细节决定你后面能不能睡个好觉。第一,UseShellExecute = false必须设,否则无法重定向输入输出。第二,输出和错误流必须异步读,因为 Spleeter 在处理时会打很多日志,如果不用BeginOutputReadLine,管道缓冲区很快被写满,子进程会阻塞,表现就是“分离到一半卡住”。第三,StandardOutputEncoding和StandardErrorEncoding在 .NET 里要显式设成 UTF-8,否则从 Python stderr 读到的FileNotFoundError会变成乱码,排查时非常痛苦。
BuildArguments这个函数是拼参数字符串,我用的是List<string>加双引号:
private static string BuildArguments(AppConfig cfg) { var args = new List<string> { $"\"{cfg.ScriptPath}\"", $"-i \"{cfg.InputFile}\"", $"-o \"{cfg.OutputDir}\"", $"-m {GetSpleeterModel(cfg.Stems)}" }; if (cfg.Bitrate > 0) args.Add($"-b {cfg.Bitrate}k"); return string.Join(" ", args); }参数说明:-i是输入音频,-o是输出目录,-m是模型名,-b是 MP3 码率。每个路径都用引号包住,是为了防 Windows 路径里的空格。很多人的程序在C:\Users\张三\My Music这类路径上莫名其妙失败,十有八九是引号没包。如果你在 C# 里用了ProcessStartInfo.ArgumentList属性,那引号可以省略,但注意这个属性只在 .NET Core 3.0 及以上才有;如果目标框架是 .NET Framework 4.8,还是老老实实用Arguments字符串拼。
3.3 进度回调与取消:Spleeter 没有百分比,但你能做出“假进度”
Spleeter 本身不输出百分比,它只在分离开始和结束时打一行Separating ...。所以 GUI 里的进度条有两个选择:要么做一个不确定进度条(ProgressBarStyle.Marquee),要么自己估算。我一般用后者:先拿 NAudio 读出音频总时长,再按 Spleeter 的大体处理速度估算,比如 10 分钟音频大约需要 40 秒,进度条每 500ms 加 1%,最后 5% 留成缓冲。这不是精确的,但用户至少知道程序没死。千万别做“到 99% 再卡 10 秒”的进度条,那会被骂。
取消也不复杂:WaitForExitAsync(token)配合CancellationTokenSource,点“取消”时先proc.Kill(entireProcessTree: true),再清理临时 WAV。注意Kill要带entireProcessTree: true,否则 Python 的子进程(比如 ffmpeg)还活着,临时文件可能被占用删不掉。这是血泪经验,我第一次没加这个参数,临时目录里堆了几十 GB 的残留音频。
3.4 输出文件的后处理:文件移动、重命名与错误日志
分离完成后,Spleeter 会在输出目录下按model_name/生成子目录,比如output/4stems/vocals.wav。这个结构对用户不友好,我一般会在 C# 里把结果移动到用户选定的文件夹,并把4stems这一层去掉。移动前先校验四个轨道文件都存在,如果缺一个,说明分离中途失败了,这时候不要用用户手动重试,直接把 stderr 日志写到一个spleeter_errors.log文件,并在界面上弹一个带日志路径的对话框。用户可以把 log 发给你,你一眼能看出是 Python 模块找不到还是 OOM。
这里有个小坑:Spleeter 输出文件名是全小写,Vocals.wav不会出现。如果你的后续流程是大小写敏感的(比如 Linux 服务器上传),没问题;但在 Windows 上,后续用 DAW 打开时没什么区别。如果做自动重命名,最好先确认目标文件名的唯一性,避免覆盖。我就是因为在移动文件时用了File.Move(src, dest, true),结果把用户之前手动整理好的同名文件直接覆盖了。现在我的习惯是:先检查目标目录是否存在同名文件,有就加_1后缀,不做覆盖。
4. 避坑指南:SpleeterGUI 最常见的 5 个运行故障与排查
下面的五条不是我从文档里抄的,而是我在维护一个内部工具时从用户反馈里一条条筛出来的。每条按现象、原因、解决的顺序写,方便你照着排查。前两条问题最隐蔽,后三条基本是环境问题。
4.1 现象:双击 exe 没反应,进程一闪而过
原因:GUI 主窗体初始化时抛了未处理异常,最常见的是目标机器缺 .NET 运行环境,或者程序集加载失败。WinForms 默认不弹窗,直接进程退出。解决:在Program.cs的Main方法里包一层全局异常捕获,把异常写入crash.log再MessageBox.Show。
[STAThread] static void Main() { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException += (s, e) => LogAndShow(e.Exception); AppDomain.CurrentDomain.UnhandledException += (s, e) => LogAndShow(e.ExceptionObject as Exception); Application.Run(new MainForm()); }这个代码虽然简单,但能救回至少一半事故。用户拿到一个 exe,双击没反应,第一反应是“这软件是坏的”;有了crash.log,你才能远程定位是缺 VC++ 运行库还是 .NET 版本不对。另外发布时建议用 .NET 自包含模式,把运行时一起打进去,虽然体积大几十 MB,但省去对方装框架的麻烦。还要检查目标平台:如果用 x86 发布,而用户系统是 x64,程序能启动但加载到某些依赖库时可能直接崩,建议一律 x64。
4.2 现象:分离结果全是金属声、机器声,人声像外星人
原因:最常见的是 TensorFlow 和 Spleeter 版本不匹配,或者模型权重下载不完整。Spleeter 官方曾有过 TensorFlow 2.x 与 1.x 的 API 差异问题,某些第三方打包的版本用了旧模型文件,推理时输出 NaN。解决:先确认 Python 环境的包版本:pip show spleeter tensorflow,然后跑官方示例音频spleeter separate -i 2stems_example.mp3 -o output -p spleeter:2stems。如果官方示例没有金属声,而你的音频有,就是输入的问题。另外,别把采样率低于 44.1kHz 的音频直接丢进去,先重采样。还有一点:处理 5-stems 时,钢琴和其他乐器在频谱上重叠多,掩码不干净,听起来会有梳状滤波感,这不是 bug,是模型局限。
我遇到过最诡异的一次:分离结果偶尔正常偶尔全是噪声,最后发现是内存不足导致 TensorFlow 的算子执行出错,但 Python 没抛异常,继续写入了 NaN 的 PCM 数据。C# 侧要主动检查输出 WAV 文件的静音段长度,如果某个轨道全是-32768或32767这种极值,就判定失败并提示用户重试。
4.3 现象:处理长音频时内存暴涨,系统卡死
原因:Spleeter 的 STFT 特征和掩码计算需要大量内存。处理 10 分钟的音频 4-stems,峰值占用可能超过 1.5GB,如果 C# GUI 本身又占一块,32 位的 exe 只有 2GB 地址空间,直接 OutOfMemory。解决:在 Visual Studio 的项目属性里把目标平台设为 x64,不要用 AnyCPU。另外在 GUI 里对输入音频长度做限制或拆分。我一般超过 15 分钟的音频会先切成 5 分钟一段,分别处理再拼接。拼接时要注意交叉淡入淡出,直接在波形中间接起来会有咔哒声;用 100ms 的交叉淡化能消除。
还有一点:Windows 的页面文件如果太小,Python 进程可能在分配内存时被杀。常见表现是 C# 的WaitForExitAsync正常返回,但 Python 退出码是负数,比如 -1073741571(超时被杀)。解决办法是让 GUI 在启动前检查系统可用内存,少于 2GB 就直接提示用户关掉其他大程序,而不是硬扛。
4.4 现象:输入路径含中文或空格时,Python 报找不到文件
原因:Windows 控制台默认代码页是 GBK,Python 3 默认 UTF-8,重定向输出时如果是 GBK,中文路径会被转得面目全非;或者 C# 拼接参数时没加引号,导致路径里的空格把参数截断。解决:所有命令行参数用双引号包住;C# 里StandardOutputEncoding设 UTF-8;Python 脚本内用os.path.abspath再解码。更稳的方案是让 Python 脚本从一个临时配置文件读取输入输出路径,而不是从命令行传。例如在 C# 里写一个 JSON 配置,Python 读取后路径就不受编码影响。这是我从一个 C# 上位机项目里学来的,传参只传配置文件路径,具体参数全放文件里,兼容性最好。
我在自己的工具里是这么做的:C# 把参数写成 UTF-8 的config.json,然后命令行只传这个文件的路径。Python 端用json.load读入,路径用pathlib.Path处理。这样即使目录叫D:\音乐\新建文件夹 (2),也不会出问题。如果坚持用命令行传参,记得在 C# 侧先用Path.GetFullPath规范化路径,再包引号。
4.5 现象:选了 GPU 反而比 CPU 慢,或者 GPU 设备找不到
原因:TensorFlow 的 GPU 支持需要 CUDA、cuDNN 版本严格匹配,而且显存不足时会自动回退到 CPU,日志里有一行Could not create CUDA device。解决:先用nvidia-smi确认显卡和驱动版本,再按 TensorFlow 官方要求装对应 CUDA。C# 侧可以在启动前检测环境:
nvidia-smi --query-gpu=name,memory.total --format=csv如果这条命令返回错误,就不要让用户勾选 GPU。另外,Spleeter 模型很小,普通 GTX 1650 和 i7 的 CPU 差距不大,短音频用 GPU 反而不值。我会在 GUI 的“GPU 加速”旁边加一句提示:“仅建议 10 分钟以上音频开启”,省得用户以为我写了个假开关。
GPU 检测的另一个坑是笔记本双显卡:TensorFlow 可能选到核显或集成显卡,导致推理极慢。C# 侧可以用CUDA_VISIBLE_DEVICES环境变量强制指定显卡序号,但不同机器的序号不一样,所以最好把显卡列表查出来让用户选。如果没有nvidia-smi,那基本可以断定这台机器不具备 TensorFlow GPU 加速条件,直接禁用 GPU 开关。
5. 实战进阶:用分离轨道做重新混音与去除环境噪声
5.1 重新混音:把 4-stems 拉进 DAW 后怎么处理
分离出的 4 轨是干声,没有混响和压缩。做 remix 时,我一般是先给鼓点轨加 transient shaper 增强打击感,贝斯轨侧链压缩到鼓点上,人声轨修一修音准再挂混响。注意 4 个轨道采样率一致,工程新建时选 44.1kHz,否则和弦对不齐。分离出的鼓点轨往往还带一点底鼓的谐波泄漏,可以用高通滤波器在 40Hz 处切掉次声波,减少共振。
5.2 去除鸟鸣、路声和喇叭声:不要直接删 other 轨
前面说过,环境声被 Spleeter 丢进 other 轨。正确做法是:先用 4-stems 分离,把 other 轨单独导出听;如果环境声集中在 other,就在 DAW 里对 other 轨做频段衰减(比如鸟鸣集中在 3-8kHz),再和原混合镜像相位相减,或者直接不输出 other 轨,只用 vocals、drums、bass 三轨重建伴奏。如果环境声也漏进了人声轨,那就只能靠 RX 的 spectral repair 修补。Spleeter 不是万能去噪器,这是模型边界,早点接受它。
更稳妥的做法是处理前先试听:把 input 拖进 DAW,用频谱图看看鸟鸣、喇叭声出现在哪个时间点,再做局部去除。SpleeterGUI 的批量模式适合直接扔十首歌,但处理现场录音时,我建议一首首看,不要迷信一键分离。
5.3 验证分离质量的土办法:相位抵消和频谱对比
我每次分离完不会急着用耳朵判断,而是用相位抵消:把原曲和分离出的所有轨道同时播放,反转其中一轨相位,如果剩下的是“空洞的混响尾音”,说明分离主要成分正确;如果听到明显的乐器残留或刮擦声,说明掩码不干净。再用频谱图看低频:贝斯轨在 200Hz 以下应该是连续的,人声轨在低频段应该很干净,否则就是交叉泄漏。这个方法不依赖 ground truth,比看 SDR 指标更直观。
我用 SpleeterGUI 有半年了,最大的教训是:永远不要在一个危险的安装环境里测试发布包。每次发新版本,我在干净的 Windows 虚拟机上跑一遍“全默认流程”,不做任何手动配置,很多用户反馈的问题其实一步就能复现。希望帮到你。
本文还有配套的精品资源,点击获取