这其实是两年前的一个产品,使用场景类似于基于苹果推出的iBeacon
实现室内定位,利用一个小巧的超声波硬件设备,周期性的广播信标信号,手机直接使用麦克风接收这个音频信号并且解码,得到信标信号中的有效数据,最后再根据这个数据进行室内定位的算法逻辑处理。声波通信部分,是技术基础,开发难度比较大,在当时的情况下,对于产品来说,是一个技术壁垒。现在已经过去两年时间了,而且其实出于商业层面的原因,团队也早已放弃这个产品转战其他方向了,所以我还是准备把其中的一些技术细节记录下来。
基于使用场景而提出的一些技术参数指标
- 只能使用18kHz~21kHz这个范围内的音频信号(人耳要尽量听不见这个声音,手机要能够接收到这个声音,所以只能限制在这个频率范围内)
- 通信距离最远要达到10米
- 还有一些是硬件设备的性能要求,和通信协议无关,不是这次的讨论重点,可以忽略
关键技术点
- 为了减小数据处理过程中的延迟现象,采取的是实时的进行音频数据采集和数据处理,而不是像某些类似SDK中使用的技术那样,先录音得到一小段音频文件,然后再进行数据处理。具体到iOS平台上,就是使用audio unit框架来搭建PCM音频数据的采集管道,在管道的最后一个节点上,对得到的PCM数据再进行进一步的处理。
- 仍然是为了降低延迟,使用手机的DSP硬件来进行快速傅里叶变换,具体到iOS上,就是用了Accelerate.framework框架中的相关函数。
- 为了提高数据传输和解码的成功率,在2FSK的基础上,做了一些调整(magic trick)。
前两点没有太多可说的,对应的开发文档中有很详尽的描述,只不过稍微偏底层一些,只要静下心来老老实实的啃啃文档,还是可以搞定的。第3点中用到的技巧,可能不太常见,我会详细解释一下。
基于标准的2FSK,假如约定用18kHz的音频信号表示二进制的0,用19kHz的音频信号表示二进制的1,同时约定每一个bit持续的发送时间为50ms,假设要发送一个8bit的二进制数据0b11001010(忽略同步和校验部分的bit),对于发送端来说,代码逻辑其实比较简单,只需要让特定频率的引号信号发送特定的时间就行了。但是对于接收端来说,代码就很困难了,虽然用的是2FSK,但是并没有专用的硬件来完成调制解调过程,所以要完全用代码来模拟整个过程,这个里面就涉及到了傅里叶变换、滤波等大量的数字信号处理里面的内容,这些处理完后,才会真正的进入到通信协议栈里面处理二进制的0和1。
如果按照标准的2FSK方式,接收端的代码必须用定时器记录0或1(18kHz或19kHz)持续的时间,然后用这个时间值和50ms做比较,才能判断出这一部分音频片段对应了多少个连续的0或1。而且这仅仅是理论上对解码算法的描述,实际情况中,发送端维持的每个bit位的持续时间是50ms,进入空气中后,会和其他的各种各样的音频信号混杂在一起,然后才进入接收端进行变换和滤波等操作,这个时候,是很难保证每个bit位仍然能够维持在50ms的(即便有50ms,代码仍然会很难编写),正式因为这些原因,成功解码数据的概率并不高。为了改善这种情况,对2FSK做了一些调整,这里借鉴了数字电路里面的一些概念和技术。在数电的串行接口电路中,使用高低电平来表示二进制的1和0,根据传输比特率的约定,每个电平会持续特定的时间,这类似于我们的音频系统中约定的每个bit持续发送50ms,这通常称为电平检测(根据电平值持续的时间进行检测),还有另外一种称为边缘检测的技术,它不依赖于每个电平值持续的时间,而是依赖于电平值的变化事件,比如电平从高变为低(从1变为0)。这里就是使用了边缘检测这种方式来处理音频信号,接收端需要关注的,是音频信号频率值的变化,而不是每个频率值持续的时间。为了实现这种方式,还需要对之前的约定做一些调整,调整为18kHz和19kHz的音频信号都可以表示二进制的0,20kHz和21kHz的音频信号都可以表示二进制的1,如果是为了表示两个连续的0,那么就应该是18kHz的音频信号持续50ms,然后变成19kHz的音频信号持续50ms(或者先发送19kHz的,再发送18kHz的),对于连续的1,也采用类似的策略。举个例子,对于二进制数据0b11101000,转换成频率值后,可能就会是这样的一组值 [20kHz,21kHz,20kHz,18kHz,21kHz,18kHz,19kHz,18kHz],因为每一个bit对应的频率值都会发生变化,那么接收端就可以忽略每个bit持续的时间,只需要检测出每一次频率值发生变化就行了,每一次变化后得到的数值,就可以对应到当前的bit位的二进制值。用了这种调制解调的思路后,接收端的代码,写起来就很容易了:]