首页 > 代码库 > 【Android开发经验】移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍(二)
【Android开发经验】移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍(二)
转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
在上一篇的文章中,我们介绍了声波通信/验证的原理和基本使用,这一篇,我们将就一些细节进行谈论。
再来一张项目的结构图
SinVoicePlayer类是我们使用的时候直接接触的类,通过调用play()方法,我们就能将需要传输的数字播放出去,下面是这个类的代码实现
/* * 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.ArrayList; import java.util.List; import android.media.AudioFormat; import android.text.TextUtils; import com.libra.sinvoice.Buffer.BufferData; /** * * @ClassName: com.libra.sinvoice.SinVoicePlayer * @Description: 声音播放类 * @author zhaokaiqiang * @date 2014-11-15 下午12:56:57 * */ public class SinVoicePlayer implements Encoder.Listener, Encoder.Callback, PcmPlayer.Listener, PcmPlayer.Callback { private final static String TAG = "SinVoicePlayer"; private final static int STATE_START = 1; private final static int STATE_STOP = 2; private final static int STATE_PENDING = 3; // 默认的间隔时间 private final static int DEFAULT_GEN_DURATION = 100; private String mCodeBook; // 用于存放使用CoodBook编码过的数字 private List<Integer> mCodes = new ArrayList<Integer>(); private Encoder mEncoder; private PcmPlayer mPlayer; private Buffer mBuffer; private int mState; private Listener mListener; private Thread mPlayThread; private Thread mEncodeThread; public static interface Listener { void onPlayStart(); void onPlayEnd(); } public SinVoicePlayer() { this(Common.DEFAULT_CODE_BOOK); } public SinVoicePlayer(String codeBook) { this(codeBook, Common.DEFAULT_SAMPLE_RATE, Common.DEFAULT_BUFFER_SIZE, Common.DEFAULT_BUFFER_COUNT); } /** * 构造函数 * * @param codeBook * @param sampleRate * 采样率 * @param bufferSize * 缓冲区体积 * @param buffCount * 缓冲区数量 */ public SinVoicePlayer(String codeBook, int sampleRate, int bufferSize, int buffCount) { mState = STATE_STOP; mBuffer = new Buffer(buffCount, bufferSize); mEncoder = new Encoder(this, sampleRate, SinGenerator.BITS_16, bufferSize); mEncoder.setListener(this); mPlayer = new PcmPlayer(this, sampleRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize); mPlayer.setListener(this); setCodeBook(codeBook); } public void setListener(Listener listener) { mListener = listener; } public void setCodeBook(String codeBook) { if (!TextUtils.isEmpty(codeBook) && codeBook.length() < Encoder.getMaxCodeCount() - 1) { mCodeBook = codeBook; } } /** * 将要加密的文本根据CodeBook进行编码 * * @param text * @return 是否编码成功 */ private boolean convertTextToCodes(String text) { boolean ret = true; if (!TextUtils.isEmpty(text)) { mCodes.clear(); mCodes.add(Common.START_TOKEN); int len = text.length(); for (int i = 0; i < len; ++i) { char ch = text.charAt(i); int index = mCodeBook.indexOf(ch); if (index > -1) { mCodes.add(index + 1); } else { ret = false; LogHelper.d(TAG, "invalidate char:" + ch); break; } } if (ret) { mCodes.add(Common.STOP_TOKEN); } } else { ret = false; } return ret; } public void play(final String text) { if (STATE_STOP == mState && null != mCodeBook && convertTextToCodes(text)) { mState = STATE_PENDING; mPlayThread = new Thread() { @Override public void run() { mPlayer.start(); } }; if (null != mPlayThread) { mPlayThread.start(); } mEncodeThread = new Thread() { @Override public void run() { mEncoder.encode(mCodes, DEFAULT_GEN_DURATION); stopPlayer(); mEncoder.stop(); mPlayer.stop(); } }; if (null != mEncodeThread) { mEncodeThread.start(); } mState = STATE_START; } } public void stop() { if (STATE_START == mState) { mState = STATE_PENDING; mEncoder.stop(); if (null != mEncodeThread) { try { mEncodeThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } finally { mEncodeThread = null; } } } } private void stopPlayer() { if (mEncoder.isStoped()) { mPlayer.stop(); } // put end buffer mBuffer.putFull(BufferData.getEmptyBuffer()); if (null != mPlayThread) { try { mPlayThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } finally { mPlayThread = null; } } mBuffer.reset(); mState = STATE_STOP; } @Override public void onStartEncode() { LogHelper.d(TAG, "onStartGen"); } @Override public void freeEncodeBuffer(BufferData buffer) { if (null != buffer) { mBuffer.putFull(buffer); } } @Override public BufferData getEncodeBuffer() { return mBuffer.getEmpty(); } @Override public void onEndEncode() { } @Override public BufferData getPlayBuffer() { return mBuffer.getFull(); } @Override public void freePlayData(BufferData data) { mBuffer.putEmpty(data); } @Override public void onPlayStart() { if (null != mListener) { mListener.onPlayStart(); } } @Override public void onPlayStop() { if (null != mListener) { mListener.onPlayEnd(); } } }
关于这个类,主要有以下几点:
1.DEFAULT_GEN_DURATION是指的每个音频信号之间的间隔,默认为0.1秒
2.convertTextToCodes()方法是将需要编码的文本进行过滤,过滤规则就是CodeBook,如果要进行传输的数字不在CodeBook里面,程序就不会继续向下执行了
虽然SinVoicePlayer类很重要,但是真正完成声音播放任务的并不是他,而是PcmPlayer类。因为源代码的一些命名很混乱很不明确,因此我修改了一些命名,如果想看原项目的同学不要感到惊讶。下面我们看一下这个类的实现。
/* * 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 android.media.AudioManager; import android.media.AudioTrack; import com.libra.sinvoice.Buffer.BufferData; /** * * @ClassName: com.libra.sinvoice.PcmPlayer * @Description: PCM播放器 * @author zhaokaiqiang * @date 2014-11-15 下午1:10:18 * */ public class PcmPlayer { private final static String TAG = "PcmPlayer"; private final static int STATE_START = 1; private final static int STATE_STOP = 2; // 播放状态,用于控制播放或者是停止 private int mState; private AudioTrack audioTrack; // 已经播放过的字节长度 private long playedLen; private PcmListener pcmListener; private PcmCallback playerCallback; public static interface PcmListener { void onPcmPlayStart(); void onPcmPlayStop(); } public static interface PcmCallback { BufferData getPlayBuffer(); void freePlayData(BufferData data); } public PcmPlayer(PcmCallback callback, int sampleRate, int channel, int format, int bufferSize) { playerCallback = callback; // 初始化AudioTrack对象(音频流类型,采样率,通道,格式,缓冲区大小,模式) audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channel, format, bufferSize, AudioTrack.MODE_STREAM); mState = STATE_STOP; } public void setListener(PcmListener listener) { pcmListener = listener; } public void start() { if (STATE_STOP == mState && null != audioTrack) { mState = STATE_START; playedLen = 0; if (null != playerCallback) { if (null != pcmListener) { pcmListener.onPcmPlayStart(); } while (STATE_START == mState) { // 获取要播放的字节数据 BufferData data = http://www.mamicode.com/playerCallback.getPlayBuffer();>
关于这个类,重点是以下几点:1.PcmPalyer是通过AudioTrack类实现单频率播放的,在初始化AudioTrack对象的时候,需要穿很多参数,我在代码里面已经注释。在SinVoicePlayer中初始化PcmPlayer对象的时候,使用的是下面的参数进行的初始化
mPlayer = new PcmPlayer(this, sampleRate, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufferSize);
sampleRate是采样率,默认44.1kHZ,AudioFormat.CHANNEL_OUT_MONO是使用单声道播放,还有立体声也就是双声道模式,为了保证频率的一致,使用单声道最合理。AudioFormat.ENCODING_PCM_16BIT是指使用16位的PCM格式编码,PCM也是一种声音的编码格式。
2.在start()方法里面的while循环是为了不断的取出要播放的字节数据,audioTrack.play()方法只会执行一次,在stop()里面把mState赋值为STATE_STOP,while循环就会退出,从而执行下面audioTrack的停止方法,结束声音的播放。
既然最后播放声音的重担落到了AudioTrack类的身上,那么我们就没有理由不去了解一下这个类了。
AudioTrack是一个用来播放声音的类,构造函数中需要传下面这些参数
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode)
重点说一下第一个和最后一个参数的含义。
AudioTrack中有MODE_STATIC和MODE_STREAM两种分类。STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样,应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。这种方式的坏处就是总是在JAVA层和Native层交互,效率损失较大。
STATIC的意思是一开始创建的时候,就把音频数据放到一个固定的buffer,然后直接传给audiotrack,后续就不用一次次得write了。AudioTrack会自己播放这个buffer中的数据。这种方法对于铃声等内存占用较小,延时要求较高的声音来说很适用。由于我们这里需要动态的写入不同的数据,因此,我们需要用MODE_STREAM模式,上面的代码中,是先write的数据,然后play(),其实正规的写法是先play(),然后通过write方法往AudioTrack里面写入字节数据即可。一开始我也疑惑呢,后来发现play方法只执行一次,而write方法会执行多次,即一边输入数据一边输出。
在构造AudioTrack的第一个参数streamType和Android中的AudioManager有关系,涉及到手机上的音频管理策略。
Android将系统的声音分为以下几类常见的:
STREAM_ALARM:警告声
STREAM_MUSCI:音乐声,例如music等
STREAM_RING:铃声
STREAM_SYSTEM:系统声音
STREAM_VOCIE_CALL:电话声音
为什么要分这么多呢?例如在听music的时候接到电话,这个时候music播放肯定会停止,此时你只能听到电话,如果你调节音量的话,这个调节肯定只对电话起作用。当电话打完了,再回到music,肯定不用再调节音量了。这可以让系统将这几种声音的数据分开管理。
这篇文章先介绍到这里,下篇文章将介绍数字编码的实现细节。
【Android开发经验】移动设备的“声波通信/验证”的实现——SinVoice开源项目介绍(二)