在Android生态中,超声波通信并不是一个新鲜概念,但受限于手机扬声器和麦克风的物理特性,真正可用的距离通常只有几米。它适合做离线设备发现、短距离配对信息交换、室内定位辅助以及无网络环境下的轻量数据传输。相比蓝牙和NFC,超声波方案的优点是无需用户确认配对、不需要额外硬件,只要调用音频API即可完成实验。不过在动手写代码之前,要先认清一个现实:手机音频链路是为了人耳设计的,高频衰减明显,因此调制方式、载波频率和接收算法都需要仔细调整。

一、理解Android音频链路并生成超声波载波
Android应用播放音频主要依靠AudioTrack类。它在Java层为PCM数据提供低延迟输出通道,适合生成连续的正弦波信号。超声波通信一般选择17kHz到22kHz之间的载波频率,低于17kHz容易被人耳感知并产生噪声,高于22kHz则可能超出部分手机扬声器或麦克风的硬件滤波范围。实际测试中,19kHz是一个比较稳妥的折中值,很多中端设备的扬声器仍能输出足够能量,同时人耳几乎听不到。
生成正弦波时,先确定采样率。推荐使用48000Hz,因为多数Android设备原生支持该采样率,避免重采样带来的失真。每个采样点的16位短整型值由公式计算:sample = amplitude * sin(2 * PI * frequency * t / sampleRate),其中振幅控制在满量程的30%到60%即可。振幅过大会触发扬声器保护电路或引入明显谐波,振幅过小则接收端信噪比不足。下面的代码创建一个可在Audiotrack上循环播放的缓冲区:
private short[] generateTone(int sampleRate, double frequency, int durationMs) {
int numSamples = sampleRate * durationMs / 1000;
short[] buffer = new short[numSamples];
double amplitude = Short.MAX_VALUE * 0.4;
for (int i = 0; i < numSamples; i++) {
double t = (double) i / sampleRate;
buffer[i] = (short) (amplitude * Math.sin(2 * Math.PI * frequency * t));
}
return buffer;
}
播放时使用AudioTrack的流模式,并通过setLoopPoints或多次写入实现连续输出。注意不同厂商的扬声器频响并不一致,某些设备在20kHz以上衰减非常明显。如果要评估目标机型,最好先播放一个扫频信号,通过录音端观察各个频点的能量差异,再选择可用载波频率。这个步骤相当于给设备做一个简单的声学标定,能显著提高后续解码成功率。
int sampleRate = 48000;
short[] tone = generateTone(sampleRate, 19000, 200);
AudioTrack track = new AudioTrack(
AudioManager.STREAM_MUSIC,
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
tone.length * 2,
AudioTrack.MODE_STATIC);
track.write(tone, 0, tone.length);
track.setLoopPoints(0, tone.length, -1);
track.play();
AudioTrack分为静态模式和流模式。静态模式一次性写入整个音频数据,适合固定长度音源;流模式边写边播,适合持续生成或不确定长度的数据。超声波测试中,如果只是回环验证,静态模式足够;如果要做实时编码发送,建议使用流模式并维护一个写入线程,避免主线程阻塞。
二、使用AudioRecord采集并完成频谱检测
接收端需要实时采集环境中的超声波信号。Android提供了AudioRecord类用于读取麦克风原始PCM数据。初始化时同样指定48000Hz、单声道、16位PCM。接收前需要在AndroidManifest.xml中声明录音权限:<uses-permission android:name="android.permission.RECORD_AUDIO" />。运行时还需要动态申请,否则初始化AudioRecord会直接失败。为了捕捉超声频段,建议使用MediaRecorder.AudioSource.VOICE_RECOGNITION或UNPROCESSED作为音频源,前者会关闭部分语音优化,后者在部分设备上可以绕过自动增益控制和降噪,尽量避免系统对高频信号做过强处理。
读取数据时采用循环缓冲,每次读取固定长度的short数组。频谱分析既可以使用FFT,也可以使用Goertzel算法。FFT适合一次性观察多个频点,例如扫频测试时分析18000Hz到22000Hz的能量分布;Goertzel更适合只判断某个已知载波是否存在,计算量小、实时性好。下面的Goertzel实现用于检测目标频率附近的能量:
public static double goertzel(short[] samples, double targetFreq, int sampleRate) {
double omega = 2 * Math.PI * targetFreq / sampleRate;
double coeff = 2 * Math.cos(omega);
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (short sample : samples) {
s0 = sample + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
double power = s1 * s1 + s2 * s2 - coeff * s1 * s2;
return Math.sqrt(power);
}
使用FFT时,频率分辨率等于采样率除以窗口长度。以48000Hz采样、2048点窗口为例,频率分辨率约为23.4Hz,足够区分500Hz间隔的FSK信号。但FFT存在频谱泄漏,如果载波频率不是频率分辨率的整数倍,能量会扩散到相邻频点。解决方法是在FFT前对时域数据加汉宁窗或海明窗,窗函数可以有效压低旁瓣,让峰值更加集中。不过加窗会略微降低主瓣高度,阈值判断时需要留出余量。
接收端常用的门槛策略是设定一个自适应阈值:先在没有信号时测量环境底噪,再取底噪均值的若干倍作为判断阈值。这样做能适应不同房间、不同设备的麦克风噪声。超声波信号在空气中的衰减较快,1米以内信号较强,超过2米后需要提高发送振幅或增加同步长度。
三、超声波通信编码与误码率测试
生成和检测单频载波只是第一步,真正用来传数据时需要引入调制方案。最简单的是OOK(开关键控),用有载波表示二进制1,无载波表示0。发送端把数据按位拆分,每个位持续固定的时长,比如40毫秒,依次播放或静音。接收端在每个位窗口内计算载波能量,超过阈值判为1,否则判为0。OOK实现简单,但对噪声敏感,而且静音段容易受到环境干扰。
FSK则用两个不同频率分别表示0和1,例如19000Hz表示1,19500Hz表示0。接收端比较两个频点的能量大小来判决。FSK比OOK更抗噪声,但需要生成两个载波并切换。无论使用哪种调制,都需要在数据前加入同步头,常见做法是先发送一段固定时间的纯载波,让接收端锁定信号起点,然后开始按位解码。下面是一段OOK同步头与位发送的伪代码:
private void sendOokBits(boolean[] bits) throws InterruptedException {
for (boolean bit : bits) {
if (bit) {
track.play();
} else {
track.pause();
}
Thread.sleep(BIT_DURATION_MS);
}
track.pause();
}
误码率测试可以在同一台设备上做回环,也可以使用两台设备。回环测试更容易排除无线传输中的衰减和混响,适合先验证算法正确性。测试时发送一段已知的随机序列,持续数秒,接收端解调后逐位比较,统计错误位数除以总位数,得到误码率。距离测试则把发送端和接收端逐渐拉开,每隔半米记录一次,观察误码率和接收能量的变化。实际测试中,手机扬声器音量、麦克风灵敏度、房间反射都会影响结果,所以同一距离应重复多次取平均。
值得注意的一点是,不同Android机型的音频输入输出延时并不一致,这会导致位同步出现偏移。可以在数据帧中加入已知的训练序列,接收端根据训练序列计算时钟偏移,再调整每个位窗口的采样位置。更可靠的做法是使用滑动窗口检测同步头,对每个可能的起点计算能量,取最大相关值作为位对齐点。
四、测试指标与常见问题排查
评估Android超声波方案时,除了误码率,还需要关注传输速率、通信距离、功耗和时延。传输速率由位持续时间决定,但位越短,接收端每个窗口内的采样点越少,抗噪声能力越差。对于常见手机硬件,40毫秒到80毫秒的位时长可以在1米左右获得比较稳定的传输,换算速率大约每秒12到25比特,适合传输短码、身份标识或控制指令,不适合传输大量文件。
测试过程中最常见的坑是系统音频特效对超声波信号的破坏。部分手机默认开启杜比、自动增益控制、回声消除或降噪,这些处理往往针对人声频段优化,会严重削弱19kHz以上的信号。对策是尽量选择UNPROCESSED音频源,并且检查系统设置中的音频增强选项。如果无法绕过,可以在代码中尝试多次切换音频源,或者改用调频连续波而非简单OOK,通过频率变化模式抵抗增益波动。
另一个容易被忽略的问题是采样率漂移。两台设备的晶振并不完全相同,48kHz可能实际播放为48010Hz,接收端按48000Hz分析就会产生频率偏移。当载波间隔较小时,这种偏移会导致FSK两个频点相互干扰。解决办法是提高频率间隔到500Hz以上,或者在接收端根据FFT峰值位置动态估计实际采样率,用估计值重新计算频点对应关系。对于实验性质的测试,先用USB声卡或已知信号源验证算法,再换成手机逐步排查,能节省大量时间。
Android超声波超声分析信号处理修改时间:2026-09-30 13:15:18