在上一篇的文章中,我们介绍了声波通信/验证的原理和基本使用,这一篇,我们将就一些细节进行谈论。
再来一张项目的结构图
SinVoicePlayer类是我们使用的时候直接接触的类,通过调用play()方法,我们就能将需要传输的数字播放出去,下面是这个类的代码实现
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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;
-
-
-
-
-
-
-
-
-
- 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;
-
- private List mCodes = new ArrayList();
-
- 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);
- }
-
-
-
-
-
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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();
- }
-
-
- 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类。因为源代码的一些命名很混乱很不明确,因此我修改了一些命名,如果想看原项目的同学不要感到惊讶。下面我们看一下这个类的实现。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- package com.libra.sinvoice;
-
- import android.media.AudioManager;
- import android.media.AudioTrack;
-
- import com.libra.sinvoice.Buffer.BufferData;
-
-
-
-
-
-
-
-
-
- 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 = 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,肯定不用再调节音量了。这可以让系统将这几种声音的数据分开管理。
这篇文章先介绍到这里,下篇文章将介绍数字编码的实现细节。
转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992