Android端TTS发声时检测他人说话的FFT实现方案咨询
基于FFT的TTS发声时人声检测方案可行性与优化
你的思路是可行的,但落地需要解决几个核心问题:回声消除、特征提取逻辑优化、实时处理流程设计,下面针对你的疑问和现有代码逐一说明:
现有代码的问题
你直接对JTransforms输出的FFT复数数组(实部+虚部交替存储)计算余弦相似度,这个逻辑存在明显缺陷:
- FFT的相位信息对语音的"身份区分"意义不大,TTS播放的声音经过空气传播、回声反射后相位会偏移,但本质还是同一个声音,直接用实虚部计算会导致相似度骤降,误判为他人说话。
- 没有分帧处理:语音是时变信号,整段音频的FFT对比没有实际意义,必须按帧(比如20ms/帧)提取特征后再做对比。
可行的实现步骤
1. 必须先做回声消除
手机麦克风会同时录到TTS的播放声(回声)和周围人声,不消除回声的话,你的对比逻辑会把自己的TTS当成"他人声音",或者根本无法区分两者:
- Android原生可以通过
AudioManager开启声学回声消除(AEC),配置AudioRecord时指定单声道、PCM16位的音频参数。 - 更可靠的方案是使用WebRTC的
AudioProcessing模块,它的回声消除算法针对移动设备做过优化,能有效滤除TTS的播放回声。
2. 特征提取优化
放弃直接用FFT复数数组,改用幅度谱或**MFCC(梅尔频率倒谱系数)**作为对比特征:
- 幅度谱:将FFT的复数转换成模长(
sqrt(实部²+虚部²)),保留频谱的能量分布信息,忽略对语音区分无用的相位。 - MFCC:专门针对语音设计的特征,能提取到语音的关键频谱特性,比原始FFT更能区分TTS和真人声。
3. 实时对比流程
- 预存TTS特征:在播放TTS前,先对TTS的原始音频数据分帧、加窗、做FFT,提取幅度谱/MFCC特征并保存。
- 实时录制与处理:用
AudioRecord实时录制周围声音,每帧执行同样的特征提取操作。 - 相似度判断:计算实时帧特征与TTS预存特征的余弦相似度,若相似度低于设定阈值(比如0.6),则判定为他人说话,触发对应事件。
优化后的相似度计算代码
针对幅度谱的余弦相似度计算逻辑:
// 将FFT复数数组转换为幅度谱数组 private fun fftComplexToMagnitude(fftComplex: DoubleArray): DoubleArray { val magnitude = DoubleArray(fftComplex.size / 2) var i = 0 while (i < fftComplex.size) { val real = fftComplex[i] val imag = fftComplex[i + 1] magnitude[i / 2] = Math.sqrt(real * real + imag * imag) i += 2 } return magnitude } // 计算幅度谱的余弦相似度 private fun calculateCosineSimilarity(mag1: DoubleArray, mag2: DoubleArray): Double { if (mag1.size != mag2.size) return 0.0 var dotProduct = 0.0 var norm1 = 0.0 var norm2 = 0.0 for (i in mag1.indices) { dotProduct += mag1[i] * mag2[i] norm1 += mag1[i] * mag1[i] norm2 += mag2[i] * mag2[i] } val denominator = Math.sqrt(norm1) * Math.sqrt(norm2) return if (denominator == 0.0) 0.0 else dotProduct / denominator }
额外注意事项
- 分帧加窗:每帧音频长度建议设为20ms,加汉明窗减少频谱泄漏。
- FFT数组大小:尽量使用2的幂次(如1024、2048),提升FFT计算效率。
- 阈值调优:实际测试时根据回声消除效果调整相似度阈值,确保TTS回声的相似度在0.8以上,他人说话时低于0.6。
- 线程处理:所有音频处理逻辑要放在子线程,避免阻塞UI线程。
内容的提问来源于stack exchange,提问作者叶辰哲
相关产品推荐
相关产品推荐

