在上一篇的文章中,我們介紹了聲波通信/驗證的原理和基本使用,這一篇,我們将就一些細節進行談論。
再來一張項目的結構圖

SinVoicePlayer類是我們使用的時候直接接觸的類,通過調用play()方法,我們就能将需要傳輸的數字播放出去,下面是這個類的代碼實作
[java] view plain copy
- 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;
- // 用于存放使用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);
- }
- 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();
- }
- // 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類。因為源代碼的一些命名很混亂很不明确,是以我修改了一些命名,如果想看原項目的同學不要感到驚訝。下面我們看一下這個類的實作。
[java] view plain copy
- 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對象(音頻流類型,采樣率,通道,格式,緩沖區大小,模式)
- 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