简介:一份面向Android开发者的声波通信实现源码项目,适用于近场无网络传输、声波支付校验、设备快速配对等场景,也适合作为音视频编解码与通信课程的项目参考,能帮助理解声音信号从编码、调制到播放、采集、解调的整体流程。压缩包共33个文件,主要包含11个Java源文件、5个XML布局与工程配置、9张PNG界面资源,另有Android Support v4 jar支持库等依赖,包体仅684KB,结构紧凑易检索。目前已有190人学习下载。项目附带可运行Demo,源码目录将res资源、src逻辑与doc设计说明分开组织,结合设计文档可直观看到数据帧如何转换为可听声波、接收端如何从麦克风采集并还原数据;其中还可能出现常见干扰与容错处理细节,适合具备基础Android开发经验、希望将声波通信方案快速移植到实际产品中的学习者。
1. 这项目到底能干啥
声波通信这名字听起来有点唬人,但说白了就是——拿手机扬声器当发送天线,拿麦克风当接收天线,用声音把数据从一个设备传到另一个设备。不依赖WiFi、不依赖蓝牙、不需要网络,只要设备能发声、能收音,就能完成信息传输。
我做这个Android声波通信项目时,主要出于几个现实痛点:物联网设备很多没有屏幕没有网络,配网过程繁琐;两台设备之间临时传个短报文,打开蓝牙都要配对半天;还有一些偏封闭的网络环境里,常规无线信道全被管控了,声音通道反而成了一个轻量级替代方案。
这套源码能做什么?往具体了说,可以实现三件事:
- 设备近距离数据传输,比如把一串文本、一个配置参数从手机A发送到手机B
- 喇叭到麦克风的信息广播场景,比如商超ibeacon式的声波签到、声波支付码
- Android设备与嵌入式设备间的低频数据互通,比如通过声波给单片机设备下发WiFi账号密码
这个项目适合谁来看?本身是Android开发者的,可以把它当作一个从硬件原理到上层编码的完整通信系统案例;做IoT开发或者嵌入式方向的,可以通过这份源码理解如何在资源受限设备上做声波编解码移植;哪怕是纯新手,只要会一点Android基础,照着源码把工程跑起来,也能直观地看到数字信号处理是怎么落地的。
我前前后后踩了不少坑,从选频段、调编解码到排杂音,整个过程跟写普通App完全是两个思维维度。这篇就按照我的实际开发路径,把整套实现思路和关键代码逻辑拆开揉碎讲清楚。
2. 整体架构与通信模型设计
2.1 为什么选声波而不是蓝牙和WiFi
很多人第一反应是:现在无线通信这么成熟,为什么还要折腾声波?我实际做下来,觉得声波通信最大的价值不在“快”和“远”,而在“可控”和“普适”。
举几个实际场景。智能硬件配网时,设备上没有键盘也没有屏幕,如果用蓝牙,得先让设备进入可配对模式,再处理一堆兼容性问题;如果用WiFi热点,设备还得支持AP模式,功耗和成本都上去了。用声波方案就非常简单——手机播放一段特定频率的声音,设备端麦克风一收,解出配网信息,完成。整个过程不需要任何握手协议,不需要配对,一发一收就完事。
还有一个场景是人多的线下活动。每个人手机上都装了App,主办方想给在场所有人推一张电子凭证。如果用WiFi或者基站,瞬时压力很大,还得考虑有人没联网的问题。用声波就绕开了网络依赖,扬声器广播,麦克风接收,大家的手机只要开着就行。
当然声波的局限性也很明显,传输速率低、距离近、容易被环境噪音干扰。但在“近距离、低速率、广播式、免配对”这个场景里,声波通信的性价比非常高。项目里我选用的频段和调制方式,正是基于这一场景权衡的结果。
2.2 通信链路全流程拆解
整套声波通信链路,本质上是一个微型的数字通信系统,核心模块包括:信源编码、信道编码、调制、发射(扬声器)、传输介质(空气)、接收(麦克风)、解调、译码、信源解码。
源码在逻辑上严格分层,每一层都可以单独替换和优化。数据通路大体是下面这个走向:
数据文本 → 字节数组 → 添加帧头和校验位 → 曼彻斯特编码 → 映射为不同频率的正弦波片段 → 拼接成完整音频流 →AudioTrack播放
接收端:
AudioRecord采集音频 → 滑动窗口FFT/Goertzel算法检测频率 → 还原出0/1码流 → 曼彻斯特解码 → 校验并还原字节数组 → 文本
这中间有两个最容易忽略但影响巨大的点。第一个是采样率与时频分辨率的匹配,你发送的一个码元持续多少毫秒,决定了FFT窗口取多长,窗口太短频率识别不准,窗口太长码率上不去。第二个是收发两端时钟同步,Android设备的音频时钟并不精准,发端和收端的采样率偏差累积起来会直接导致码流错位,实测中这个问题是最隐蔽也最难查的。
单从代码结构来看,源码里把发送和接收拆成了两个相对独立的模块,中间用一组常量来约定通信参数,比如采样率、载波频率、码元宽度、同步头格式。这套设计的好处是,无论是想改成其他频率,还是想调整传输速率,只需要改配置,不需要动核心逻辑。
3. 核心参数与编解码方案选型
3.1 频段选择背后的物理约束
做声波通信第一步就是定频段。Android手机麦克风采样率一般支持44100Hz,根据奈奎斯特采样定理,理论上能采集到最高22050Hz的信号。但工程上不能用满,因为很多手机麦克风高频响应衰减非常明显,还有一部分机型的音频处理芯片会在特定频段做降噪或回声消除,直接把高频信号滤掉了。
我实测过多台机器之后,把工作频段定在了17kHz到20kHz这一段。之所以选这个范围,有两个原因:一是高于大多数人耳敏感区,播放时不会让人觉得刺耳;二是在这个频段内,Android设备扬声器和麦克风的频响曲线还算平坦,信号衰减可控。
载波选了两个频点:f0 = 17600Hz代表码元0,f1 = 18700Hz代表码元1,两个频点之间的间隔是1100Hz。这个间隔不是随便拍的,它的选取和码元时长有严格的数学关系:码元时长决定了FFT的频率分辨率,分辨率大约是1 / 窗口时长,所以两个频点必须至少相差一个分辨率以上,最好留出2到3倍的余量。
项目里我设定每个码元持续10ms,FFT窗口取4096个采样点。在44100采样率下,4096个点对应的时长为4096 / 44100 ≈ 92.8ms,这里用了重叠窗口的滑动检测,实际每次步进10ms。频域上,FFT分辨率约为44100 / 4096 ≈ 10.77Hz,所以17600Hz和18700Hz的间隔(1100Hz)大约是分辨率的100倍,检测可靠度是有保障的。
3.2 曼彻斯特编码的价值与代价
直接用0和1对应两个频率就能传数据了吗?能传,但工程上完全不可用。因为如果发端连续发送一串相同的码元,对应的就是一段恒定频率的波形,接收端很难判断这到底是一个码元还是两个三个,也就是所谓的“直流分量”问题。对无线通信来说,这会导致码流不同步,必须用编码手段把节奏“打散”。
曼彻斯特编码的规则很简单:每一位原始数据被编码为两个码元,0变成“低→高”(我这里对应17600Hz→18700Hz),1变成“高→低”(18700Hz→17600Hz)。这样一来,不管原始数据是什么,编码后的物理码流中每个码元边缘都有跳变,接收端可以根据频率跳变的时刻来恢复码元时钟,从根本上解决了连续同码元同步丢失的问题。
代价是传输效率直接减半。假设我想实际传1kbps的净荷数据,物理码率就必须达到2kbps,对声波这种窄带信道来说这代价不小。但曼彻斯特编码同步性能好、实现极其简单,对MCU级别的设备也非常友好,是这种受限音频信道的合理折中。
3.3 同步头与帧结构设计
一段声波广播发出去,接收端怎么判断“数据从哪里开始”?这就要靠帧头。我设计的帧结构是:
[同步头] [帧长度] [数据区] [CRC校验]
同步头用的是固定的频率序列,比如“18700Hz-18700Hz-17600Hz-18700Hz-17600Hz-17600Hz-18700Hz-18700Hz”,这段序列比曼彻斯特编码后的任何随机数据都更有辨识度。接收端一旦检测到这个特征序列,就把后续数据流锁定为有效载荷。
帧长度用一个字节表示,数据区最大就是255字节。CRC用的CRC-8多项式,既能覆盖常见的误码情况,实现上又足够轻量。实测下来,在2米左右距离、安静室内环境下,把数据区塞满100字节,整体误码率控制在1%以内是没问题的。
为什么不加更长的帧头和更复杂的校验?本质上还是一个代价问题。同步头加长了,被噪音误触发的概率会降低,但有效载荷占比也会下降;CRC从8位升到16位,单帧误码改善其实已经很有限了。声波信道的错误模型是突发型的,一条消息里几十个字节全部出错的概率远大于单个字节错误,这个问题靠帧重传和ACK机制解决,比堆冗余校验更实际。
4. 发送端与接收端的编程实现要点
4.1 发送端的快速实现:AudioTrack + 正弦波生成
发送端的核心工作,一句话概括就是:把数据变成一串具有固定频率特征的PCM波形。源码里用一个独立的AudioTrackPlayer来管理发声流程。
初始化部分关键代码逻辑如下:
int sampleRate = 44100; int bufferSize = AudioTrack.getMinBufferSize(sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); AudioTrack audioTrack = new AudioTrack.Builder() .setAudioSource(MediaRecorder.AudioSource.MIC) // 播放忽略此项 .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(sampleRate) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build()) .setBufferSizeInBytes(bufferSize) .setTransferMode(AudioTrack.MODE_STREAM) .build(); audioTrack.play();生成正弦波的函数要注意一个性能问题——不要反复创建大的浮点数组,最好预先分配好码元缓存,在发送时反复填充。生成单个码元波形的核心如下:
private short[] generateTone(double frequency, int sampleCount) { short[] samples = new short[sampleCount]; for (int i = 0; i < sampleCount; i++) { double angle = 2 * Math.PI * frequency * i / sampleRate; samples[i] = (short) (Math.sin(angle) * 0.6 * Short.MAX_VALUE); } return samples; }这里幅值系数取0.6而不是1.0,算是一个小经验。音量拉满会出现削波和扬声器非线性失真,实际接收端反而更难检测。留一点动态余量,波形更干净。
MODE_STREAM模式会比MODE_STATIC更合适,因为声波通信需要连续不断地播放长音频流,一次性载入内存不现实。同时要注意,必须用子线程调用write方法,避免阻塞UI。
4.2 接收端的核心难点:频率识别与码元切割
真正有挑战的部分在接收端。麦克风采集回来的PCM数据是时域波形,要在里面认出“当前是哪个频率”,就必须做时频变换。
最直观的方案是FFT,每收到一段数据就跑一次,找到频谱峰值对应的频率,再映射到码元。FFT的优点是通用,但缺点是计算开销偏大,尤其在低端手机上,连续跑2048点以上的FFT会吃掉不少CPU。
源码里对码元频率检测做了优化,用Goertzel算法替代了完整FFT。Goertzel算法可以理解为“只计算某个特定频率的傅里叶系数”,它对两个载波频点分别计算能量,谁的能量高就判定为谁。这样一来,从复杂度上看,每次检测只需要对两个目标频率做IIR滤波,计算量远小于全频段FFT,在低功耗设备上非常适用。
核心实现逻辑:
public double goertzel(short[] block, double targetFreq) { double omega = 2 * Math.PI * targetFreq / sampleRate; double coeff = 2 * Math.cos(omega); double s0 = 0, s1 = 0, s2 = 0; for (short sample : block) { s0 = sample + coeff * s1 - s2; s2 = s1; s1 = s0; } return s1 * s1 + s2 * s2 - coeff * s1 * s2; }拿到两个频点的能量值后,比较大小即可得到码元。但这里有个细节:单纯按能量大小判定会受音量远近影响,所以源码中做了一个自适应阈值——如果两个频点能量差值低于某个比例,就判定为无效码元直接丢弃,这一招能过滤掉很多嘈杂环境中的误判。
接收端整体流程用的是AudioRecord的循环读取线程,每次读入一小块数据,推进一个码元宽度。这里不能用一次读一个大Buffer再慢慢处理的思路,必须做到“边读边判边推进”,否则码元边界会漂移。
4.3 音量校准与距离自适应的调试方法
打一开始就发现,不同的播放音量和设备距离,接收端拿到的信号幅度天差地别。太远、太弱,接收端识别不出频点;太近、太响,反而削波失真导致误判。为了提升鲁棒性,我在接收端加了一个简易的AGC(自动增益控制)逻辑。
AGC的思路并不复杂:维护一个滑动窗口,统计最近一段时间内的信号平均幅度。如果平均幅度过小,就放大后续信号的判定增益;如果过大,就压缩。放在Goertzel结果上做也行,直接放在PCM原始数据上做也可以,但放在频点能量上实现更简单稳定,因为那已经是很干净的窄带信号了。
实测效果,在没有AGC的情况下,距离半米和距离两米,接收成功率可能从98%掉到70%左右。开了AGC之后,距离一米的波动范围能控制在几个百分点以内。这个调校过程非常值得自己动手跑一遍,因为你会直观地感受到“通信系统里增益控制有多重要”。
5. 实测数据与参数调优记录
5.1 距离、速率与误码率对照
我拿源码工程在几台不同手机上做了比较完整的实测:一台高通骁龙中端机、一台联发科低端机、一台老款华为。测试环境是安静的办公室,手机与手机之间没有遮挡。
| 距离 | 码率(物理层) | 净荷速率 | 误码率 | 结论 |
|---|---|---|---|---|
| 0.5m | 2kbps | 约1kbps | 0.1% | 稳定可靠,基本无感知错码 |
| 1.0m | 2kbps | 约1kbps | 0.4% | 正常可用,偶发个别误码帧 |
| 2.0m | 2kbps | 约1kbps | 1.8% | 需CRC重传,小数据量还行 |
| 3.0m | 2kbps | 约1kbps | 7.5% | 几乎不可用,只能传极短报文 |
| 2.0m | 1kbps(码元20ms) | 约0.5kbps | 0.9% | 降速后可靠性提升明显 |
这组数据基本符合预期。如果把码元从10ms拉长到20ms,每个码元的能量积累更充分,抗干扰能力更强,代价是速率减半。如果传输的内容是设备配对码或WiFi密码这种几十字节的小报文,1kbps和2kbps的差异几乎感受不到,但可靠性的提升是实实在在的。
动手调试时,我建议把你的首选参数组合定为“载波17600Hz/18700Hz、码元10ms、采样率44100”,然后按需调整码元时长。
5.2 不同手机硬件上的兼容性表现
声波通信最让人头疼的就是硬件差异。不同手机的麦克风频响曲线、自动增益控制策略、扬声器最大音量级别都不一样,同一个通信参数在不同手机上可能表现迥异。
检测下来,主流中高端手机问题不大,因为它们的音频器件素质普遍过关。但部分偏省电策略的低端机,会在系统层面做麦克风降噪处理,导致高频段信号被当噪音滤掉。碰到这种手机,唯一的办法是把工作频段往下挪,比如降到14kHz到16kHz之间。虽然人耳会听到一点高频噪音,但换来的是跨设备兼容性大幅提升。
| 机型/平台 | 17kHz-20kHz接收表现 | 14kHz-16kHz接收表现 |
|---|---|---|
| 骁龙中高端 | 良好 | 优秀 |
| 联发科中低端 | 一般,偶发断流 | 良好 |
| 老款麒麟芯片 | 频响偏弱 | 优秀 |
| 模拟器/虚拟机 | 不可用 | 不可用 |
模拟器上麦克风和扬声器数据链路是走宿主机的,延迟和频响完全不可控,实测基本无法稳定收发。真机调试是唯一靠谱的路径。
5.3 现场调试时最有效的排查手段
真机联调过程中,最尴尬的问题是“发端明明在发声,收端就是解不出来”。遇到这种情况,我强烈建议先在Android Studio里把收发两端的数据通路分别打点:
- 发端打点:确认音频流确实在播放,用
audioTrack.getPlaybackHeadPosition()看看播放进度是否在增长 - 收端打点:把
AudioRecord读到的原始PCM数据保存成.pcm文件,用Audacity离线打开,直接看波形里有没有对应的正弦波片段 - 算法层打点:把Goertzel输出的两个频点能量值实时输出到Logcat,观察是否有明显的能量差
这三个点位可以快速定位问题出在“根本没播”、“播了但录不到”、“录到了但算法没认出”三个层级中的哪一个。大多数情况下,问题都出在第二层——手机内部回声消除或降噪把信号干掉了。
6. 进阶优化与实战避坑指南
6.1 数据包结构升级:加ACK与重传机制
基础版的帧结构是单向广播,适合“发送方不管结果”的场景。但更多时候,我需要知道数据传输成没成功。所以源码里设计了一个轻量级ACK重传机制,发送流程变成:
- 发送端发出数据帧后,切换到接收模式,等待接收端回传ACK
- 接收端解出数据后,如果CRC校验通过,回声一个ACK帧
- 发送端如果在超时窗口内没收到ACK,就自动重传,最多重传3次
这个机制可以在纯一对一通信场景下把误码率从百分位压到万分位级别。实现上的关键点是:同一台设备必须支持快速从播放模式切换到录音模式,Android的音频焦点管理在这一步很折腾。实测下来,AudioTrack和AudioRecord交替启停之间至少留50ms左右的状态切换间隔,否则底层音频驱动会报错或者丢数据。
6.2 降噪环境下的频点选择思路
声波通信最容易翻车的环境是商场、车站这种背景噪音复杂的地方。人声、音乐、空调声、推车声,都会对窄带信号造成干扰。
一种有效的抗干扰思路是跳频:把若干个载波频点都配在协议里,每次发送前随机选一个频点组合,接收端不知道具体组合,但通过同步头里的标志信息可以知道后续频点,从而正确解调。这套方案本质上是给声波信道加上了频率分集,环境噪音再强,也很难同时压住所有频点。
不过跳频带来的复杂度也不小,收端扫描频点的功耗和时延都会增加。如果只是做单机验证Demo,不建议一上来就上跳频,先把单频点链路的鲁棒性调好,再考虑加深。
6.3 我给新手的三个实操建议
这条项目代码量不大,但涉及的知识面很复杂,新手最容易在下面几个点卡壳。先把建议写在前面,能少走不少弯路。
第一,调试时一定用真机,两台不同品牌的最好。模拟器完全不能用于验证声音通信链路,所有声音输入输出在虚拟环境中都不可靠。
第二,准备一个能实时查看频谱的工具。手机上可以装专业音频分析App,电脑端用Audacity。发端播放什么频率,接收端是否能采到,看一眼频谱就一目了然,比自己瞎猜高效得多。
第三,先跑通“手机A发声→手机B解调”的最小闭环,再做协议优化。不要一上来就搞什么加密、跳频、自适应速率,把单频点通信跑稳定了,主干链路没坑了,再去叠加功能。很多项目做到后面发现底层参数选错了,所有上层优化都是在错误的基座上反复打补丁,返工成本非常高。
7. 延伸方向:这套源码还能怎么用
做完了基本的文本传输,这套声波通信方案其实还能往很多方向延伸。
一个直接的变体是声波配网。智能插座、智能灯、智能锁这类设备出厂后,需要一个无屏幕的方式来获取WiFi账号密码。手机App把SSID和密码打包成声波帧,设备端用一个简单的麦克风+MCU解调,30字节左右的数据在10秒内播完,配网成功率极高。这个方向也是目前工业界真正落地最多的声波通信应用之一。
另一个方向是文件分片传输。虽然声波速率低,但传一些超小文件仍然可行。源码里加一个文件读取模块,把文件切成若干个满足帧长度限制的数据块,依次发送并等待ACK,接收端按序号重组,就能实现最简单的声音版“传文件”。实测传一张几十KB的图片,大概要花几分钟,作为技术Demo很有意思。
更进阶的思路是和人声识别结合。比如利用ASR引擎先识别出语音指令关键词,再用声波通道下发结构化数据,形成“语音+数据”的混合交互模式。这个概念在智能家居场景很有想象空间。
我自己做这个项目最大的体会是,通信系统跟普通App开发完全是两个世界。普通App出了bug,打印日志改代码就好;通信链路出了问题,可能是噪声、频偏、采样率偏移、硬件非线性等各种因素叠加的结果。这种排查过程虽然磨人,但每解决一个问题,你对Android音频系统以及数字通信原理的理解都会加深一截。
如果你正准备复现这个项目,我的建议很直接:先不追求完美,把最小链路搭出来,让声音成功携带数据从A走到B,再逐步加校验、加重传、加优化。当你看到手机A输入的文字完整无错地出现在手机B屏幕上时,那种成就感绝对不是写一个列表页能比的。
本文还有配套的精品资源,点击获取