Android开发经验移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍

将相本无种,男儿当自强。这篇文章主要讲述Android开发经验移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍相关的知识,希望能为你提供帮助。
    转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
    前两篇介绍了声波验证/通信的原理和声音播放的实现,这一篇将介绍最重要。也是最难懂的东西,就是SinVoice是怎样对这些数字进行编码传输的。

    由于源码中增加了大量的难以区分的回调函数。为了阅读方便,我进行了部分的重命名和代码的整理,大家不要感到诧异。

    首先给出项目的结构:
     

Android开发经验移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍

文章图片



    这篇文章重点介绍是Encoder类、SinGenerator类,Buffer类。
    在前面的文章中,我们了解到SinVoiceplayer是我们直接接触和使用的类,使用SinVoicePlayer.play(text)方法就能够非常easy的播放出我们想要传输的数字的相应音频信号。然后进行解析就可以。
在SinVoicePlayer中,是通过调用PcmPlayer的start()方法,进行播放操作的,而在pcmPlayer中,则是又调用AudioTrace来实现终于的音频播放功能。
通过对AudioTrace的一层层封装,终于实现了SinVoicePlayer的简单调用。
    既然AudioTrace是终于进行音频播放的类。那么,要进行播放的数据从哪里来的呢?
    答案就是,数据来自Encoder类、SinGenerator类和Buffer类。

    以下是Encoder的代码,代码经过了整理

/* * Copyright (C) 2013 gujicheng * * Licensed under the GPL License Version 2.0; * you may not use this file except in compliance with the License. * * If you have any question, please contact me. * ************************************************************************* **Author information** ************************************************************************* ** Email: [email  protected]** ** QQ: 29600731** ** Weibo: http://weibo.com/gujicheng197** ************************************************************************* */ package com.libra.sinvoice; import java.util.List; import com.libra.sinvoice.Buffer.BufferData; /** * * @ClassName: com.libra.sinvoice.Encoder * @Description: 编码器 * @author zhaokaiqiang * @date 2014-11-16 下午1:32:17 * */ public class Encoder implements SinGenerator.SinGeneratorCallback { private final static String TAG = " Encoder" ; private final static int STATE_ENCODING = 1; private final static int STATE_STOPED = 2; // index 0, 1, 2, 3, 4, 5, 6 // circleCount 31, 28, 25, 22, 19, 15, 10 private final static int[] CODE_FREQUENCY = { 1422, 1575, 1764, 2004, 2321, 2940, 4410 }; private int mState; private SinGenerator mSinGenerator; private EncoderCallback encoderCallback; public static interface EncoderCallback {void freeEncodeBuffer(BufferData buffer); BufferData getEncodeBuffer(); } public Encoder(EncoderCallback callback, int sampleRate, int bits, int bufferSize) { encoderCallback = callback; mState = STATE_STOPED; mSinGenerator = new SinGenerator(this, sampleRate, bits, bufferSize); } public final static int getMaxCodeCount() { return CODE_FREQUENCY.length; } public final boolean isStoped() { return (STATE_STOPED == mState); } // content of input from 0 to (CODE_FREQUENCY.length-1) public void encode(List< Integer> codes, int duration) { if (STATE_STOPED == mState) { mState = STATE_ENCODING; mSinGenerator.start(); for (int index : codes) { if (STATE_ENCODING == mState) { if (index > = 0 & & index < CODE_FREQUENCY.length) { // 使用正弦发生器编码 mSinGenerator.gen(CODE_FREQUENCY[index], duration); } else { LogHelper.d(TAG, " code index error" ); } } else { LogHelper.d(TAG, " encode force stop" ); break; } } mSinGenerator.stop(); } } public void stop() { if (STATE_ENCODING == mState) { mState = STATE_STOPED; mSinGenerator.stop(); } } @Override public BufferData getGenBuffer() { if (null != encoderCallback) { return encoderCallback.getEncodeBuffer(); } return null; } @Override public void freeGenBuffer(BufferData buffer) { if (null != encoderCallback) { encoderCallback.freeEncodeBuffer(buffer); } } }


    关于这个类,主要是以下几点:
    1.这个类实现了SinGenerator.SinGeneratorCallback接口,这个接口事实上是SinGenerator类里面的。这个接口主要完毕的数据的获取与释放,在以下的代码中我将会说明
【Android开发经验移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍】    2.数组CODE_FREQUENCY中存放的数字分别代表从0到6相应的频率,不同的数据会依据不同的频率进行编码,circleCount 31, 28, 25, 22, 19, 15, 10 是指在编码的过程中,相应频率的正弦波在一个周期内的取样数量。每个周期的取样数量x周期总数=总取样数。
    还记得之前的DEFAULT_GEN_DURATION=100吗,这个变量指的就是每个数字相应的音频持续的时间,100代表100毫秒,也就是0.1秒。
我们说过,默认的採样率使用的是44.1KHZ,这是1s内採样44100次。假设须要播放100毫秒,那么就仅仅须要採样44100/10=4410次,由于
private final static int[] CODE_FREQUENCY = { 1422, 1575, 1764, 2004, 2321,2940, 4410 };
    假如我们想给数字0编码。那么我们知道0相应的振动频率就是1422HZ。这个也是一秒钟的。假设是100ms呢?就是142.2HZ,我们使用,142.2x31=4408.2,接近4410次,所以说,我们依据想要生成的音频的频率。就能知道每个周期内须要採样多少次,就能算出採样的间隔。
由于Encoder也仅仅是一个包装类,真正实现编码的是SinGenerator,在这个累里面,我们就能够看到非常多的加密细节。

    以下是SinGenerator的代码实现

/* * Copyright (C) 2013 gujicheng * * Licensed under the GPL License Version 2.0; * you may not use this file except in compliance with the License. * * If you have any question, please contact me. * ************************************************************************* **Author information** ************************************************************************* ** Email: [email  protected]** ** QQ: 29600731** ** Weibo: http://weibo.com/gujicheng197** ************************************************************************* */ package com.libra.sinvoice; import com.libra.sinvoice.Buffer.BufferData; /** * * @ClassName: com.libra.sinvoice.SinGenerator * @Description: 正弦波发生器 * @author zhaokaiqiang * @date 2014-11-15 下午2:51:34 * */ public class SinGenerator { private static final String TAG = " SinGenerator" ; private static final int STATE_START = 1; private static final int STATE_STOP = 2; // 2^8时的峰值 public static final int BITS_8 = 128; // 默觉得2^16时的峰值 public static final int BITS_16 = 32768; // 採样率 public static final int SAMPLE_RATE_8 = 8000; public static final int SAMPLE_RATE_11 = 11250; public static final int SAMPLE_RATE_16 = 16000; public static final int UNIT_ACCURACY_1 = 4; public static final int UNIT_ACCURACY_2 = 8; private int mState; private int mSampleRate; private int mBits; private static final int DEFAULT_BITS = BITS_8; private static final int DEFAULT_SAMPLE_RATE = SAMPLE_RATE_8; private static final int DEFAULT_BUFFER_SIZE = 1024; private int mFilledSize; private int mBufferSize; private SinGeneratorCallback sinGeneratorCallback; public static interface SinGeneratorCallback {BufferData getGenBuffer(); void freeGenBuffer(BufferData buffer); } public SinGenerator(SinGeneratorCallback callback) { this(callback, DEFAULT_SAMPLE_RATE, DEFAULT_BITS, DEFAULT_BUFFER_SIZE); } public SinGenerator(SinGeneratorCallback callback, int sampleRate, int bits, int bufferSize) { sinGeneratorCallback = callback; mBufferSize = bufferSize; mSampleRate = sampleRate; mBits = bits; mFilledSize = 0; mState = STATE_STOP; } public void stop() { if (STATE_START == mState) { mState = STATE_STOP; } } public void start() { if (STATE_STOP == mState) { mState = STATE_START; } } /** * 对数字进行编码 * * @param genRate * @param duration */ public void gen(int genRate, int duration) { if (STATE_START == mState) {// 定值16384 int n = mBits / 2; int totalCount = (duration * mSampleRate) / 1000; double per = (genRate / (double) mSampleRate) * 2 * Math.PI; double d = 0; LogHelper.d(TAG, " per:" + per + " ___genRate:" + genRate); if (null != sinGeneratorCallback) { mFilledSize = 0; // 获取要编码的数据 BufferData bufferData = https://www.songbingjia.com/android/sinGeneratorCallback.getGenBuffer(); if (null != bufferData) { for (int i = 0; i < totalCount; ++i) {if (STATE_START == mState) {// 算出不同点的正弦值 int out = (int) (Math.sin(d) * n) + 128; // 假设填充数量超过了缓冲区的大小,就重置mFilledSize,释放bufferData if (mFilledSize > = mBufferSize - 1) { // free buffer bufferData.setFilledSize(mFilledSize); sinGeneratorCallback.freeGenBuffer(bufferData); mFilledSize = 0; bufferData = sinGeneratorCallback .getGenBuffer(); if (null == bufferData) { LogHelper.d(TAG, " get null buffer" ); break; } }// 转码为byte类型并保存。& 0xff是为了防止负数转换出现异常 bufferData.byteData[mFilledSize++] = (byte) (out & 0xff); if (BITS_16 == mBits) { bufferData.byteData[mFilledSize++] = (byte) ((out > > 8) & 0xff); }d += per; } else { LogHelper.d(TAG, " sin gen force stop" ); break; } } } else { LogHelper.d(TAG, " get null buffer" ); }if (null != bufferData) { bufferData.setFilledSize(mFilledSize); sinGeneratorCallback.freeGenBuffer(bufferData); } mFilledSize = 0; } } } }


    最基本的方法就是gen(),我们对这种方法进行具体的解析。
    1int n = mBits / 2; 在这里定义的n。在后面的代码中參与了运算,n是指我们要创建的正弦函数的峰值,就是最高点的值,mBits的值是2^16/2=32768,在这里将峰值除以二,应该是为了识别率考虑,由于在将n直接赋值为mBits的时候,发出的声音较为尖锐,识别率减少非常多,所以,这里选择了mBits/2最为峰值。

    2.int totalCount = (duration * mSampleRate) / 1000; 这个是在计算要循环的次数,由于duration=100,所以採样的总次数是4410,循环运行4410次
    3.double per = (genRate / (double) mSampleRate) * 2 * Math.PI; 这个per參数是用来记录在循环的过程中,每次往前步进的距离,这个是和频率相关的。
我们以发出数字5为例。从Encoder类中。我们知道5相应的频率是2940HZ,假设我们要声音播放100ms,那么就须要震动294次。也就是294个正弦周期。
而这294次。依据44.1KHZ的频率。也就是100ms採样4410次的频率。就能够算出在每个周期里面,须要採样4410/294=15,所以一个周期内採样数量是15次。而一个正弦周期的长度是2PI。所以,使用2PI/15=0.4186。这个值就是这里的per值。

    4.int out = (int) (Math.sin(d) * n) + 128; 在计算出per之后。在循环中使用变量d对每次的per进行了累加,然后使用前面的计算公式,就能够计算出在採样点处相应的函数值,在完毕以下的操作之后,就实现了数字的编码

// 转码为byte类型并保存。& 0xff是为了防止负数转换出现异常
bufferData.byteData[mFilledSize++] = (byte) (out & 0xff);
if (BITS_16 == mBits) {
bufferData.byteData[mFilledSize++] = (byte) ((out > > 8) & 0xff);
}


    我们看到,在保存编码的时候,使用了Buffre类,这个类是对字节数据进行存储的类。用来保存编码完毕之后的字节数据。以下我们简单看下这个类的代码

/* * Copyright (C) 2013 gujicheng * * Licensed under the GPL License Version 2.0; * you may not use this file except in compliance with the License. * * If you have any question, please contact me. * ************************************************************************* **Author information** ************************************************************************* ** Email: [email  protected]** ** QQ: 29600731** ** Weibo: http://weibo.com/gujicheng197** ************************************************************************* */ package com.libra.sinvoice; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * * @ClassName: com.libra.sinvoice.Buffer * @Description: 缓冲器 * @author zhaokaiqiang * @date 2014-11-15 下午1:35:46 * */ public class Buffer { private final static String TAG = " Buffer" ; // 生产者队列 private BlockingQueue< BufferData> mProducerQueue; // 消费者队列 private BlockingQueue< BufferData> mConsumeQueue; // 缓冲区数量 private int mBufferCount; // 缓冲区体积 private int mBufferSize; public Buffer() { this(Common.DEFAULT_BUFFER_COUNT, Common.DEFAULT_BUFFER_SIZE); } public Buffer(int bufferCount, int bufferSize) {mBufferSize = bufferSize; mBufferCount = bufferCount; mProducerQueue = new LinkedBlockingQueue< BufferData> (mBufferCount); // we want to put the end buffer, so need to add 1 mConsumeQueue = new LinkedBlockingQueue< BufferData> (mBufferCount + 1); // 初始化生产者队列 for (int i = 0; i < mBufferCount; ++i) { try { mProducerQueue.put(new BufferData(mBufferSize)); } catch (InterruptedException e) { e.printStackTrace(); } } } public void reset() { // 将生产者的空头结点剔除 int size = mProducerQueue.size(); for (int i = 0; i < size; ++i) { BufferData data = https://www.songbingjia.com/android/mProducerQueue.peek(); if (null == data || null == data.byteData) { mProducerQueue.poll(); } }// 将消费者中的非空数据增加到生产者其中 size = mConsumeQueue.size(); for (int i = 0; i < size; ++i) { BufferData data = mConsumeQueue.poll(); if (null != data & & null != data.byteData) { mProducerQueue.add(data); } }LogHelper.d(TAG, " reset ProducerQueue Size:" + mProducerQueue.size() + " ConsumeQueue Size:" + mConsumeQueue.size()); } final public int getEmptyCount() { return mProducerQueue.size(); } final public int getFullCount() { return mConsumeQueue.size(); } // 获取生产者的头结点,堵塞式 public BufferData getEmpty() { return getImpl(mProducerQueue); } // 增加到生产者中 public boolean putEmpty(BufferData data) { return putImpl(data, mProducerQueue); } // 获取消费者的头结点 public BufferData getFull() { return getImpl(mConsumeQueue); } // 增加到消费者中 public boolean putFull(BufferData data) { return putImpl(data, mConsumeQueue); } // 获取队列的头结点 private BufferData getImpl(BlockingQueue< BufferData> queue) { if (null != queue) { try { return queue.take(); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } // 将数据增加到队列中 private boolean putImpl(BufferData data, BlockingQueue< BufferData> queue) { if (null != queue & & null != data) { try { queue.put(data); return true; } catch (InterruptedException e) { e.printStackTrace(); } } return false; } // when mData is null, means it is end of input public static class BufferData { // 数据容器 public byte byteData[]; // 填充体积 private int mFilledSize; // 缓冲最大体积 private int mMaxBufferSize; // 静态空缓冲区 private static BufferData sEmptyBuffer = new BufferData(0); public BufferData(int maxBufferSize) {mMaxBufferSize = maxBufferSize; mFilledSize = 0; if (maxBufferSize > 0) { byteData = new byte[mMaxBufferSize]; } else { byteData = null; } }/** * 获取空的缓冲区 * * @return */ public static BufferData getEmptyBuffer() { return sEmptyBuffer; }// 重置填充数量 final public void reset() { mFilledSize = 0; }final public int getMaxBufferSize() { return mMaxBufferSize; }// 设置填充数量 final public void setFilledSize(int size) { mFilledSize = size; }final public int getFilledSize() { return mFilledSize; } }}



    Buffer使用两个队列实现了生产者消费者模型,从而保证编译好一个。播放一个,在SinVoicePlayer类里面开了两个线程,分别对两个队列里面的数据进行管理。

     
    这个项目的Demo下载地址https://github.com/ZhaoKaiQiang/SinVoiceDemo

    推荐阅读