【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: [email protected]                                         **
 ** 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: [email protected]                                         **
 ** 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,肯定不用再调节音量了。这可以让系统将这几种声音的数据分开管理。


    这篇文章先介绍到这里,下篇文章将介绍数字编码的实现细节。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。