如何在Android上准确测试音频失真?

来源:IPIPP.com作者:卡拉米头衔:草根站长
导读:本期聚焦于卡拉米创作的《如何在Android上准确测试音频失真?》,敬请观看详情。为什么播放同一条1kHz正弦波,几台Android设备测出来的失真值能差出好几倍?要想把音频失真测试做得稳定,不能只盯一个THD百分比。Android链路里采样率转换、自动增益、音效处理和扬声器非线性都在影响结果。本文介绍总谐波失真THD与THD+N的差别,说明基于AudioTrack播放标准正弦波、AudioRecord采集回放信号的闭环测试方法,并给出谐波幅值计算和失真提取的代码实现。测试时建议关闭AGC和音效,优先使用UNPROCESSED音频源,同时注意窗函数、谐波次数和参考设备校准。掌握这些要点,才能得到有参考价值的Android音频失真数据。

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

如何在Android上准确测试音频失真?

一、失真指标: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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0921/60179.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。