在上一篇的文章中,我們介紹了聲波通信/驗證的原理和基本使用,這一篇,我們將就一些細節進行談論。
再來一張項目的結構圖
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,肯定不用再調節音量了。這可以讓系統將這幾種聲音的數據分開管理。
這篇文章先介紹到這裏,下篇文章將介紹數字編碼的實現細節。