【Android開發經驗】行動裝置的“聲波通訊/驗證”的實現——SinVoice開源項目介紹(三),androidsinvoice
轉載請註明出處:http://blog.csdn.net/zhaokaiqiang1992
前兩篇介紹了聲波驗證/通訊的原理和聲音播放的實現,這一篇將介紹最重要,也是最難懂的東西,就是SinVoice是如何對這些數字進行編碼傳輸的。
因為原始碼中加入了大量的難以區分的回呼函數,為了閱讀方便,我進行了部分的重新命名和代碼的整理,大家不要感到詫異。
首先給出項目的結構:
這篇文章重點介紹是Encoder類、SinGenerator類,Buffer類。
在前面的文章中,我們瞭解到SinVoiceplayer是我們直接接觸和使用的類,使用SinVoicePlayer.play(text)方法就可以非常容易的播放出我們想要傳輸的數位對應音頻訊號,然後進行解析即可。在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: gujicheng197@126.com ** ** 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, 10private 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();}}@Overridepublic BufferData getGenBuffer() {if (null != encoderCallback) {return encoderCallback.getEncodeBuffer();}return null;}@Overridepublic void freeGenBuffer(BufferData buffer) {if (null != encoderCallback) {encoderCallback.freeEncodeBuffer(buffer);}}}
關於這個類,主要是以下幾點:
1.這個類實現了SinGenerator.SinGeneratorCallback介面,這個介面其實是SinGenerator類裡面的,這個介面主要完成的資料的擷取與釋放,在下面的代碼中我將會說明
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: gujicheng197@126.com ** ** 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) {// 定值16384int 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 = 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,釋放bufferDataif (mFilledSize >= mBufferSize - 1) {// free bufferbufferData.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: gujicheng197@126.com ** ** 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 1mConsumeQueue = 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 = 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 inputpublic 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類裡面開了兩個線程,分別對兩個隊列裡面的資料進行管理。
這個項目的Demohttps://github.com/ZhaoKaiQiang/SinVoiceDemo