简介一份面向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代表码元0f1 18700Hz代表码元1两个频点之间的间隔是1100Hz。这个间隔不是随便拍的它的选取和码元时长有严格的数学关系码元时长决定了FFT的频率分辨率分辨率大约是1 / 窗口时长所以两个频点必须至少相差一个分辨率以上最好留出2到3倍的余量。项目里我设定每个码元持续10msFFT窗口取4096个采样点。在44100采样率下4096个点对应的时长为4096 / 44100 ≈ 92.8ms这里用了重叠窗口的滑动检测实际每次步进10ms。频域上FFT分辨率约为44100 / 4096 ≈ 10.77Hz所以17600Hz和18700Hz的间隔1100Hz大约是分辨率的100倍检测可靠度是有保障的。3.2 曼彻斯特编码的价值与代价直接用0和1对应两个频率就能传数据了吗能传但工程上完全不可用。因为如果发端连续发送一串相同的码元对应的就是一段恒定频率的波形接收端很难判断这到底是一个码元还是两个三个也就是所谓的“直流分量”问题。对无线通信来说这会导致码流不同步必须用编码手段把节奏“打散”。曼彻斯特编码的规则很简单每一位原始数据被编码为两个码元0变成“低→高”我这里对应17600Hz→18700Hz1变成“高→低”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.5m2kbps约1kbps0.1%稳定可靠基本无感知错码1.0m2kbps约1kbps0.4%正常可用偶发个别误码帧2.0m2kbps约1kbps1.8%需CRC重传小数据量还行3.0m2kbps约1kbps7.5%几乎不可用只能传极短报文2.0m1kbps码元20ms约0.5kbps0.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屏幕上时那种成就感绝对不是写一个列表页能比的。本文还有配套的精品资源点击获取