查看: 111|回复: 3

鸿蒙端侧人声分离实战:Spleeter ONNX模型与ONNX Runt

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
提词器播背景音乐时,ASR会把伴奏里的歌声当成台词,导致语音翻页完全失效。解决思路很直接:把一首歌拆成伴奏和人声两轨,保留伴奏当BGM,去掉人声,这样麦克风只收台词,ASR匹配准了。人声分离在AI领域不算新,但如何在鸿蒙上实现?我翻遍社区没有现成教程,于是自己动手。

技术选型:为什么选Spleeter
主流方案中,Spleeter的2-stem模型(人声+伴奏)只有两个ONNX文件,每个约15MB,共30MB,手机应用可以接受。分离质量在安静环境下不错,对提词器场景够用。Demucs效果更好但模型太大,端侧太重;Open-Unmix效果一般。Spleeter是折中方案。关键点是,原始模型是TensorFlow格式,需转成ONNX才能在鸿蒙上用ONNX Runtime跑。

整体架构:两层结构
人声分离没有声纹识别那么复杂,就两层:C++ DSP层(stem_spleeter_dsp.cpp)实现STFT/FFT/iSTFT/OLA等纯信号处理,不依赖AI框架;C++ ONNX层(stem_spleeter_ort.cpp)负责WAV解析、频谱特征提取、ONNX推理、Wiener归一化、信号重构和WAV输出。ArkTS层(StemSpleeterOnnxService.ets)只做模型文件搬运和NAPI调用。两个功能共用同一个.so文件(libspeakerverification.so),因为都依赖ONNX Runtime。CMakeLists.txt里把三个cpp编进一个共享库。为什么不拆成两个.so?鸿蒙HAP包每多一个.so就多一份体积,共享ORT实例比加载两份ORT省内存。

DSP层:手搓FFT和STFT
stem_spleeter_dsp.cpp约180行代码,实现完整的STFT→iSTFT信号处理链。FFT用经典的Cooley-Tukey蝶形算法,不依赖第三方库。FFT点数为4096(kNfft=4096),对应频率分辨率44100/4096≈10.77Hz,对人声分离刚好。STFT参数在头文件定义:采样率44100Hz(必须,Spleeter原始训练数据就是44100Hz)、帧移1024样本、窗长4096样本(等于FFT点数)、有效频率bin数1024、时间分块512帧。帧移1024意味着相邻帧有75%重叠——重叠越大,重构相位连续性越好,但计算量成倍增加,这是经典trade-off。

iSTFT使用Overlap-Add(OLA)方法还原时域信号,关键步骤是用win_sum数组记录每个采样点的窗函数覆盖能量,最后除以该能量做归一化,避免75%重叠区域幅度膨胀。很多教程会省略这一步,不除会导致分离出来的音频声音巨大、削波严重。

ONNX层:从WAV到分离
stem_spleeter_ort.cpp约470行,是最核心也最复杂的文件。流程分几步:

1. WAV解析:手写解析器,只支持PCM16格式的WAV。如果是单声道,自动复制成双声道(模型按双声道训练)。采样率必须44100Hz,否则直接返回错误(硬校验)。

2. 频谱特征提取:STFT后得到左右声道的复数频谱,计算幅度谱作为ONNX模型输入。将幅度谱重排成tensor格式(2, splits, 512, 1024):2个声道,按512帧分块,每块1024个频率bin。音频长度不是512帧整数倍时补零。

3. ONNX推理:需要两个独立Session,分别加载人声模型和伴奏模型(架构一样但权重不同)。推理时共享同一个输入(原始幅度谱),但输出不同:人声模型输出人声mask,伴奏模型输出伴奏mask。每个Session设置1个线程,避免抢CPU。推理一次约2~5秒(取决于音频长度)。

4. Wiener归一化:两个模型输出的mask理论上相加应等于1,但实际有误差。Wiener归一化修正这个问题,保证两轨能量之和等于原始音频。不做归一化会有“幽灵声”——人声轨残留伴奏,伴奏轨残留人声。

5. 信号重构:用归一化后的mask乘原始频谱,得到分离后频谱,再通过iSTFT+OLA转回时域。

6. WAV输出:浮点PCM转int16时做硬限幅(clamp到[-1,1]),防止int16溢出产生爆音。

模型转换:TensorFlow→ONNX
Spleeter原始模型是.h5格式,转换步骤:
- pip install tf2onnx
- 用tf2onnx将.h5转成.onnx
- 用onnxruntime量化工具做FP16量化,减小体积
最终得到vocals.fp16.onnx和accompaniment.fp16.onnx,每个约15MB。模型文件放在resources/rawfile/spleeter/下,ArkTS层在运行时解压到沙箱。

ArkTS层:简单但有坑
StemSpleeterOnnxService.ets的逻辑很简单——从rawfile拷贝两个ONNX文件到沙箱,然后调NAPI初始化。这里与声纹识别的模型拷贝方式不同:声纹模型大(24MB),需用getRawFdSync分块拷贝;人声分离每个模型只有15MB,在getRawFileContentSync安全范围内,直接用整包读取。如果模型更大(如Demucs的80MB),必须改成分块读取。

模型文件有完整性检查:如果沙箱里文件大小与rawfile不一致,就重新拷贝。这个检查很必要——App更新后rawfile里模型换了新版,沙箱仍缓存旧版,输入shape不一致会导致推理崩溃。

踩坑记录
坑1:输入不是44100Hz直接崩。一开始没做采样率校验,拿16000Hz音频去分离,STFT帧数与模型期望对不上,推理报shape mismatch。后来加了硬校验,如果输入不是44100Hz直接返回错误,不做重采样(重采样影响质量且增加代码量)。

坑2:Wiener归一化前后的音质差异。第一版没加Wiener归一化,分离后仔细听人声轨有“嘶嘶”的伴奏残留,伴奏轨有人声“哼鸣”。一开始以为是模型不行,换权重也没用。后来加了Wiener归一化,残留几乎消失。原理是模型输出的是logits而非概率,归一化后映射到概率空间保证能量守恒。

坑3:单声道输入的处理。用户反馈单声道音频分离后两轨声音一模一样。原因是单声道STFT后左右频谱完全相同,模型输入一样输出自然一样。解决方案是单声道复制成双声道再处理。虽然不增加信息量,但模型是按双声道训练的,输入格式不对会出问题。

反面教材:两个模型用一个Session跑。起初想只创建一个ORT Session,先跑人声模型再跑伴奏模型,但Ort::Session构造时锁定计算图和权重,运行时不能换权重。必须创建两个独立的Session分别加载不同模型。尽管多占一倍内存,但这是Spleeter架构决定的。

与声纹识别的共用设计
人声分离和声纹识别共享同一个.so,共享ORT运行时,但模型完全独立。初始化函数也不同:speakerVerification.initExtractor负责声纹,speakerVerification.initSpleeter负责人声分离。共用ORT的好处是节省内存(Ort::Env全局单例),坏处是一个功能的Session出问题可能影响另一个。代码里每个功能独立try-catch和错误码,互不干扰。

效果与建议
在Mate 60 Pro上测试分离3分钟44100Hz立体声WAV:
- 模型加载:约500ms(首次,拷贝+创建两个Session)
- 推理时间:约3~5秒
- 输出文件:人声WAV+伴奏WAV,大小与原文件相当
- 分离质量:安静环境下人声干净度约85%~90%,伴奏残留很少,提词器场景够用。

几点建议:
- 模型文件放rawfile,体积大用getRawFdSync分块拷贝,别用getRawFileContentSync(大文件会OOM)。
- Wiener归一化不能省,模型输出mask不是概率,直接用有残留。
- 输入格式严格校验:只认44100Hz/PCM16/stereo WAV,入口处校验比运行时crash强。
- 与声纹共用.so合理,但要确保初始化和错误处理独立。

人声分离代码量不大但信号处理部分硬核,如果不熟悉FFT/STFT,建议先补数字信号处理基础,否则代码里的参数(4096、1024、1024、512)看起来只是魔法数字。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙端侧人声分离实战:Spleeter ONNX模型与ONNX Runt

感谢楼主分享这么详实的鸿蒙端侧人声分离实战经验!从技术选型到代码架构,再到踩坑记录和反例分析,非常系统。 Spleeter 2-stem模型30MB、两层结构拆分明晰,尤其是DSP层手搓FFT不依赖第三方库,对端侧优化很友好。STFT参数中75%重叠和Wiener归一化这两个细节,如果没有说明,很多人可能调试很久也找不到问题根源。 对于单声道复制成双声道才正常这一点,我之前也踩过类似的坑——模型按双声道训练,单声道输入格式不匹配会导致输出稀奇古怪。另外两个模型必须独立Session这个“反面教材”也很关键,ORT的设计决定了不能在运行时换权重。 想请教一个小问题:ArkTS层用getRawFileContentSync整包读取15MB模型,在低端设备上会不会有短时内存抖动?另外你提到采样率硬校验不重采样,如果用户传入了其他采样率(如48kHz),是直接提示用户转换吗?希望后续能看到更多鸿蒙上的端侧AI落地分享!
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙端侧人声分离实战:Spleeter ONNX模型与ONNX Runt

感谢楼主的详细分享!之前在鸿蒙上做人声分离一直没找到现成方案,你这篇实操记录太及时了。Spleeter+ONNX的选型确实很务实,30MB模型在端侧能接受,拆分DSP和ONNX层的设计思路也清晰,尤其是手写FFT不用第三方库,既减体积又可控性高。 有几个问题想请教: 1. 模型转换时做FP16量化,精度损失在实测中对分离效果影响大吗?有没有对比过FP32版本? 2. 你提到两个Session各1个线程,但ONNX Runtime的线程数设置会不会受设备核心数限制?在麒麟芯片上实测单次推理2~5秒,对于提词器这种实时场景会不会有延迟感? 3. Wiener归一化这块,除了消除残留,有没有试过直接对mask做softmax之类的归一化?效果和Wiener归一化相比如何? 4. 单声道复制成双声道再处理,虽然模型输入对了,但分离后左右声道完全一致,后续是否有考虑把两轨混合成单声道输出节省存储? 再次感谢,这贴解决了我之前“单声道分离结果两轨一样”的疑惑,原来硬件校验和复制操作都要到位。期待后续更新!
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 鸿蒙端侧人声分离实战:Spleeter ONNX模型与ONNX Runt

非常感谢楼主分享这么详细的实战经验!从技术选型到具体实现,再到踩坑记录,内容非常扎实。特别是关于Wiener归一化对音质的影响、单声道输入处理的细节,以及两个模型必须用独立Session这些点,对后来者太有参考价值了。 想请教一下,您在模型量化到FP16后,在鸿蒙端侧推理速度(2~5秒)是针对多长音频的?另外,如果输入音频采样率不是44100Hz,您没有加重采样,那有没有考虑过在ArkTS层提前用系统API转成44100Hz再传下去呢?再次感谢分享!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-22 15:34 , Processed in 0.028735 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部