Android音频失真测试并不是简单播放一个正弦波再录回来算个百分比。整条链路包含应用层生成PCM、AudioTrack播放、扬声器或耳机输出、麦克风采集、AudioRecord回传,以及最后的频谱分析。任何一个环节的非线性、自动增益或采样率转换,都会把测量结果推向不可信的方向。所以做失真测试前,要先明确测的是整机链路失真,还是芯片或算法模块失真。

一、失真指标:THD与THD+N不能混用
总谐波失真THD描述的是谐波能量与基波能量的比值。假设输入一个1kHz正弦波,理想输出只应包含1kHz分量;但真实系统会产生2kHz、3kHz、4kHz等整数倍谐波。把这些谐波的方和根除以基波幅值,再换算成百分比或dB,就是THD。公式里的基波通常用输入频率的实际输出幅值,而不是理论幅值。
THD+N则在此基础上把噪声也纳入口径,常见表示为信号与谐波加噪声的比值。严格说THD+N比THD更贴近主观听感,因为宽带噪声、电源纹波、抖动都可能被计入。Android设备上底噪并不稳定,很多低端机型在无输入时也会产生明显宽带噪声,所以如果只报告THD,有时会把噪声影响隐藏起来。测试报告里建议同时注明THD和THD+N,并说明带宽范围,否则数值没有可比性。
不同厂商的音频框架还会做动态范围压缩和EQ处理。部分设备默认开启杜比、蝰蛇或系统级音效,音乐播放路径并非透明传输。这些处理会显著改变谐波结构。例如动态压缩在低电平时提高增益,会让噪声和谐波一起增大。因此测试时优先使用未经过音效处理的通路,或者至少在报告中说明测试模式。
二、搭建闭环测试链路
闭环测试的基本流程是:先用代码生成指定频率和幅度的PCM正弦波,通过AudioTrack播放;再用AudioRecord同步采集扬声器或耳机回放的声音。播放和采集的采样率最好保持一致,比如都使用48kHz、单声道、16位PCM。采样率不一致时系统会插入重采样器,重采样本身可能引入镜像频率和带内纹波,这些最终会被误判为失真。
生成正弦波时,幅度不要直接拉满。满幅0dBFS在后续重采样和模拟输出阶段容易触发削波,产生大量高次谐波。通常建议使用-6dBFS或-3dBFS的幅度。下面的代码生成一个1kHz、-6dBFS、持续1秒的单声道正弦波,并写入AudioTrack静态模式。
private short[] generateSineWave(int sampleRate, double frequency, int durationMs, double amplitude) {
int numSamples = sampleRate * durationMs / 1000;
short[] pcm = new short[numSamples];
for (int i = 0; i < numSamples; i++) {
double sample = amplitude * Math.sin(2.0 * Math.PI * frequency * i / sampleRate);
pcm[i] = (short) sample;
}
return pcm;
}
private void playTestTone() {
int sampleRate = 48000;
double amplitude = 32767 * 0.501; // 约-6dBFS
short[] pcm = generateSineWave(sampleRate, 1000.0, 1000, amplitude);
int minBuf = AudioTrack.getMinBufferSize(sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
AudioTrack track = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setBufferSizeInBytes(minBuf)
.setTransferMode(AudioTrack.MODE_STATIC)
.build();
track.write(pcm, 0, pcm.length);
track.play();
}
采集端要尽量避开系统自带的AGC、降噪和回声消除。Android提供MediaRecorder.AudioSource.UNPROCESSED源,它绕过大部分前处理,但只支持部分设备,需要先调用AudioManager.getProperty或捕获异常来判断是否可用。如果不可用,可以退回VOICE_RECOGNITION或DEFAULT,但要明白这些源可能包含降噪和AGC。录音前还要设置录音权限,并保持设备稳定,避免手持移动造成多普勒或反射变化。
下面是使用AudioRecord采集PCM的示例。实际测试中建议让播放和录制在两个线程中协调,录制开始时间早于播放,采集长度覆盖完整波形,避免剪切起始段。
private short[] recordTestSignal(int durationMs) {
int sampleRate = 48000;
int minBuf = AudioRecord.getMinBufferSize(sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
AudioRecord recorder = new AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.UNPROCESSED)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build())
.setBufferSizeInBytes(minBuf * 2)
.build();
int total = sampleRate * durationMs / 1000;
short[] pcm = new short[total];
recorder.startRecording();
int offset = 0;
while (offset < total) {
int read = recorder.read(pcm, offset, total - offset, AudioRecord.READ_BLOCKING);
if (read <= 0) break;
offset += read;
}
recorder.stop();
recorder.release();
return pcm;
}
如果采集到的信号幅度很小,需要先确认播放音量和麦克风增益。自动增益控制会动态改变录音增益,使不同设备之间的输入电平不一致。测试失真时更希望固定增益,否则AGC把低电平放大,会把底噪也放大,最后算出的THD+N会很差。部分工程模式或企业设备支持关闭AGC,普通App可以通过选择UNPROCESSED源来绕过部分自动处理。
三、谐波幅值计算与失真值提取
拿到PCM数据后,需要计算基波和各次谐波的幅度。完整FFT能给出全频带频谱,但失真测试往往只关心基波和2到10次谐波。采用Goertzel算法或在这些频点做DFT,比完整FFT更快,也更适合嵌入式环境。下面的代码使用汉宁窗降低频谱泄漏,并计算指定频率的幅度。
计算窗口需要覆盖整数个周期,否则单频点DFT会因频谱泄漏把能量分散到相邻频点,导致幅值偏低。实际操作中可以让测试信号时长包含整数个周期,例如48kHz采样率、1kHz信号,选取48000个采样点刚好覆盖1000个周期。这样即使不加窗,泄漏也很小。如果无法控制整周期,可以用顶部平坦窗或提高FFT点数来减小误差。
private double computeAmplitude(short[] samples, int sampleRate, double targetFreq) {
int usable = samples.length;
double real = 0.0;
double imag = 0.0;
double omega = 2.0 * Math.PI * targetFreq / sampleRate;
for (int i = 0; i < usable; i++) {
double window = 0.5 * (1.0 - Math.cos(2.0 * Math.PI * i / (usable - 1)));
double windowed = window * samples[i];
real += windowed * Math.cos(omega * i);
imag -= windowed * Math.sin(omega * i);
}
return 2.0 * Math.sqrt(real * real + imag * imag) / usable;
}
private double calculateThd(short[] samples, int sampleRate, double fundamentalFreq) {
double fundamental = computeAmplitude(samples, sampleRate, fundamentalFreq);
if (fundamental < 1e-9) return 0.0;
double harmonicPowerSum = 0.0;
for (int h = 2; h <= 10; h++) {
double amp = computeAmplitude(samples, sampleRate, fundamentalFreq * h);
harmonicPowerSum += amp * amp;
}
return Math.sqrt(harmonicPowerSum) / fundamental * 100.0;
}
代码中只累加谐波幅值平方,没有把噪声功率计入,因此得到的是THD。如果要计算THD+N,需要先滤除基波和谐波,再计算剩余信号的总功率。也可以从完整FFT频谱中扣除基波与谐波所在频点,再对剩余频点能量求和。实际测量时谐波次数通常取到10次就足够,因为更高次谐波能量很小,且可能被噪声淹没。测试带宽建议限定在20Hz到20kHz,避免超声和直流分量影响结果。
算出的THD百分比只反映当前电平和设备状态。为了得到稳定数据,可以连续测试10次取中位数,同时观察标准差。如果标准差很大,往往说明AGC在动、系统音效在切换,或者录制缓冲区有丢帧。Android设备在低电量和发热状态下CPU调度会变化,音频线程可能被抢占,导致录制数据出现不连续。测试前最好关闭蓝牙、Wi-Fi扫描和后台应用。
四、容易被忽略的校准与误差控制
绝对失真值在不同设备之间很难完全公平。麦克风和扬声器本身就有非线性,如果使用外放测试,扬声器的THD常常远大于芯片或算法模块的失真。要评估某个算法模块,应该使用音频线回录代替扬声器外放,或者使用USB音频接口作为参考输入输出。只有在相同连接方式、相同电平和相同采样率下,设备之间的THD差异才有分析价值。
另外要注意重采样误差。很多Android设备内部固定使用48kHz或44.1kHz,如果播放文件是44.1kHz而系统输出是48kHz,重采样会引入带外镜像。虽然好的重采样器会压低镜像,但低端设备可能使用线性插值,带内失真明显上升。测试时可以统一生成48kHz信号,避开重采样。也可以分别用44.1kHz和48kHz各测一次,如果THD差异很大,说明重采样器质量较差。
最后给一个校准思路:先在一台已知音频性能良好的USB声卡上完成同样测试,记录THD基线;然后在目标Android设备上用相同设置测试,把两者差值作为相对失真。更严谨的做法是使用标准失真仪或Audio Precision设备做交叉验证,但日常开发中至少保留一台参考机,定期复测,避免因为系统更新或音频参数变化导致数据漂移。把测试链路、电平、窗函数和谐波次数都固化到脚本里,才能保证Android音频失真测试结果可复现。
Android音频失真THD+NFFT分析修改时间:2026-09-21 19:49:32