转载请注明出处: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 = playerCallback.getPlayBuffer();
if (null != data) {
if (null != data.byteData) {
// 设置要播放的字节数据
int len = audioTrack.write(data.byteData, 0,
data.getFilledSize());
// 首次进入,播放声音
if (0 == playedLen) {
audioTrack.play();
}
playedLen += len;
// 释放数据
playerCallback.freePlayData(data);
} else {
LogHelper.d(TAG,
"it is the end of input, so need stop");
break;
}
} else {
LogHelper.d(TAG, "get null data");
break;
}
}
if (STATE_STOP == mState) {
audioTrack.pause();
audioTrack.flush();
audioTrack.stop();
}
if (null != pcmListener) {
pcmListener.onPcmPlayStop();
}
} else {
throw new IllegalArgumentException("PcmCallback can't be null");
}
}
}
public void stop() {
if (STATE_START == mState && null != audioTrack) {
mState = STATE_STOP;
}
}
}
关于这个类,需要注意的是以下几点:
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,肯定不用再调节音量了。这可以让系统将这几种声音的数据分开管理。
这篇文章先介绍到这里,下篇文章将介绍数字编码的实现细节。