AudioRecord和AudioTrack類是Android獲取和播放音頻流的重要類,放置在android.media包中。與該包中的MediaRecorder和MediaPlayer類不同,AudioRecord和AudioTrack類在獲取和播放音頻數據流時無需通過文件保存和文件讀取,可以動態地直接獲取和播放音頻流,在實時處理音頻數據流時非常有用。
當然,如果用戶只想錄音後寫入文件或從文件中取得音頻流進行播放,那麼直接使用MediaRecorder和MediaPlayer類是首選方案,因為這兩個類使用非常方便,而且成功率很高。而AudioRecord和AudioTrack類的使用卻比較復雜,我們發現很多人都不能成功地使用這兩個類,甚至認為Android的這兩個類是不能工作的。
其實,AudioRecord和AudioTrack類的使用雖然比較復雜,但是可以工作,我們不僅可以很好地使用了這兩個類,而且還通過套接字(Socket)實現了音頻數據的網絡傳輸,做到了一端使用AudioRecord獲取音頻流然後通過套接字傳輸出去,而另一端通過套接字接收後使用AudioTrack類播放。
下面是我們對AudioRecord和AudioTrack類在使用方面的經驗總結:
(1)創建AudioRecord和AudioTrack類對象:創建這兩個類的對象比較復雜,通過對文檔的反復和仔細理解,並通過多次失敗的嘗試,並在北理工的某個Android大牛的網上的文章啟發下,我們也最終成功地創建了這兩個類的對象。創建AudioRecord和AudioTrack類對象的代碼如下:
AudioRecord類:
m_in_buf_size =AudioRecord.getMinBufferSize(8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
m_in_rec = new AudioRecord(MediaRecorder.AudioSource.MIC,
8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
m_in_buf_size) ;
AudioTrack類:
m_out_buf_size = android.media.AudioTrack.getMinBufferSize(8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
m_out_trk = new AudioTrack(AudioManager.STREAM_MUSIC, 8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
m_out_buf_size,
AudioTrack.MODE_STREAM);
(2)關於AudioRecord和AudioTrack類的監聽函數,不用也行。
(3)調試方面,包括初始化後看logcat信息,以確定類的工作狀態,初始化是否成功等。
編寫好代碼,沒有語法錯誤,調用模擬器運行、調試代碼時,logcat發揮了很好的功用。剛調試時,經常會出現模擬器顯示出現異常,這時我們可以在代碼的一些關鍵語句後添加如Log.d("test1","OK");這樣的語句進行標識,出現異常時我們就可以在logcat窗口觀察代碼執行到哪裡出現異常,然後進行相應的修改、調試。模擬器不會出現異常時,又遇到了錄放音的問題。錄音方面,剛開始選擇將語音編碼數據存放在多個固定大小的文件中進行傳送,但是這種情況下會出現聲音斷續的現象,而且要反復的建立文件,比較麻煩,後來想到要進行網上傳輸,直接將語音編碼數據以數據流的形式傳送,經過驗證,這種方法可行並且使代碼更加簡潔。放音方面,將接收到的數據流存放在一個數組中,然後將數組中數據寫到AudioTrack中。剛開始只是“嘟”幾聲,經過檢查發現只是把數據寫一次,加入循環,讓數據反復寫到AudioTrack中,就可以聽到正常的語音了。接下來的工作主要是改善話音質量與話音延遲,在進行通話的過程中,觀察logcat窗口,發現向數組中寫數據時會出現Bufferflow的情況,於是把重心轉移到數組大小的影響上,經過試驗,發現 AudioRecord一次會讀640個數據,然後就對錄音和放音中有數組的地方進行實驗修改。AudioRecord和AudioTrack進行實例化時,參數中各有一個數組大小,經過試驗這個數組大小和AudioRecord和AudioTrack能正常實例化所需的最小Buffer大小(即上面實例化時的m_in_buf_size和m_out_buf_size參數)相等且服務器方進行緩存數據的數組尺寸是上述數值的2倍時,語音質量最好。由於錄音和放音的速度不一致,受到北理工大牛的啟發,在錄音方面,將存放錄音數據的數組放到LinkedList中,當LinkedList中數組個數達到2(這個也是經過試驗驗證話音質量最好時的數據)時,將先錄好的數組中數據傳送出去。經過上述反復試驗和修改,最終使雙方通話質量較好,且延時較短(大概有2秒鐘)。
(4)通過套接字傳輸和接收數據
數據傳送部分,使用的是套接字。通信雙方,通過不同的端口向服務器發送請求,與服務器連接上後,開始通話向服務器發送數據,服務器通過一個套接字接收到一方的數據後,先存在一個數組中,然後將該數組中數據以數據流的形式再通過另一個套接字傳送到另一方。這樣就實現了雙方數據的傳送。
(5)代碼架構
為避免反復錄入和讀取數據占用較多資源,使程序在進行錄放音時不能執行其他命令,故將錄音和放音各寫成一個線程類,然後在主程序中,通過MENU控制通話的開始、停止、結束。
最後說明,AudioRecord和AudioTrack類可以用,只是稍微復雜些。以下貼出雙方通信的源碼,希望對大家有所幫助:
主程序Daudioclient:
package cn.Daudioclient;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class Daudioclient extends Activity {
public static final int MENU_START_ID = Menu.FIRST ;
public static final int MENU_STOP_ID = Menu.FIRST + 1 ;
public static final int MENU_EXIT_ID = Menu.FIRST + 2 ;
protected Saudioserver m_player ;
protected Saudioclient m_recorder ;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public boolean onCreateOptionsMenu(Menu aMenu)
{
boolean res = super.onCreateOptionsMenu(aMenu) ;
aMenu.add(0, MENU_START_ID, 0, "START") ;
aMenu.add(0, MENU_STOP_ID, 0, "STOP") ;
aMenu.add(0, MENU_EXIT_ID, 0, "EXIT") ;
return res ;
}
public boolean onOptionsItemSelected(MenuItem aMenuItem)
{
switch (aMenuItem.getItemId()) {
case MENU_START_ID:
{
m_player = new Saudioserver() ;
m_recorder = new Saudioclient() ;
m_player.init() ;
m_recorder.init() ;
m_recorder.start() ;
m_player.start() ;
}
break ;
case MENU_STOP_ID:
{
m_recorder.free() ;
m_player.free() ;
m_player = null ;
m_recorder = null ;
}
break ;
case MENU_EXIT_ID:
{
int pid = android.os.Process.myPid() ;
android.os.Process.killProcess(pid) ;
}
break ;
default:
break ;
}
return super.onOptionsItemSelected(aMenuItem);
}
}
錄音程序Saudioclient:
package cn.Daudioclient;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
public class Saudioclient extends Thread
{
protected AudioRecord m_in_rec ;
protected int m_in_buf_size ;
protected byte [] m_in_bytes ;
protected boolean m_keep_running ;
protected Socket s;
protected DataOutputStream dout;
protected LinkedList<byte[]> m_in_q ;
public void run()
{
try
{
byte [] bytes_pkg ;
m_in_rec.startRecording() ;
while(m_keep_running)
{
m_in_rec.read(m_in_bytes, 0, m_in_buf_size) ;
bytes_pkg = m_in_bytes.clone() ;
if(m_in_q.size() >= 2)
{
dout.write(m_in_q.removeFirst() , 0, m_in_q.removeFirst() .length);
}
m_in_q.add(bytes_pkg) ;
}
m_in_rec.stop() ;
m_in_rec = null ;
m_in_bytes = null ;
dout.close();
}
catch(Exception e)
{
e.printStackTrace();
}
}
public void init()
{
m_in_buf_size = AudioRecord.getMinBufferSize(8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
m_in_rec = new AudioRecord(MediaRecorder.AudioSource.MIC,
8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
m_in_buf_size) ;
m_in_bytes = new byte [m_in_buf_size] ;
m_keep_running = true ;
m_in_q=new LinkedList<byte[]>();
try
{
s=new Socket("192.168.1.100",4332);
dout=new DataOutputStream(s.getOutputStream());
//new Thread(R1).start();
}
catch (UnknownHostException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void free()
{
m_keep_running = false ;
try {
Thread.sleep(1000) ;
} catch(Exception e) {
Log.d("sleep exceptions...\n","") ;
}
}
}
放音程序Saudioserver:
package cn.Daudioclient;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.util.Log;
public class Saudioserver extends Thread
{
protected AudioTrack m_out_trk ;
protected int m_out_buf_size ;
protected byte [] m_out_bytes ;
protected boolean m_keep_running ;
private Socket s;
private DataInputStream din;
public void init()
{
try
{
s=new Socket("192.168.1.100",4331);
din=new DataInputStream(s.getInputStream());
m_keep_running = true ;
m_out_buf_size = AudioTrack.getMinBufferSize(8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
m_out_trk = new AudioTrack(AudioManager.STREAM_MUSIC, 8000,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT,
m_out_buf_size,
AudioTrack.MODE_STREAM);
m_out_bytes=new byte[m_out_buf_size];
// new Thread(R1).start();
}
catch(Exception e)
{
e.printStackTrace();
}
}
public void free()
{
m_keep_running = false ;
try {
Thread.sleep(1000) ;
} catch(Exception e) {
Log.d("sleep exceptions...\n","") ;
}
}
public void run()
{
byte [] bytes_pkg = null ;
m_out_trk.play() ;
while(m_keep_running) {
try
{
din.read(m_out_bytes);
bytes_pkg = m_out_bytes.clone() ;
m_out_trk.write(bytes_pkg, 0, bytes_pkg.length) ;
}
catch(Exception e)
{
e.printStackTrace();
}
}
m_out_trk.stop() ;
m_out_trk = null ;
try {
din.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
在AndroidManifest.xml文件中加上下面的聲明獲取權限就行了,一個是獲得網絡傳輸套接字的權限,一個是獲取音頻數據流的權限
<uses-permission
android:name="android.permission.INTERNET">
</uses-permission>
<uses-permissionandroid:name="android.permission.RECORD_AUDIO">
</uses-permission>
通過套接字傳輸數據是非常簡單的,按照普通JAVA的類和對象等的概念,首先建立套接字類的對象,然後使用對象的函數即可。在上面的代碼中有,現貼在下面供參考,其中s就是套接字的對象,按照這個線索仔細研究其實很簡單,關鍵是要知道類中函數的具體功能。
private Socket s;//聲明套接字對象
private DataInputStream din;
public void init()
{
try
{
s=new Socket("192.168.1.100",4331);//建立對象具體內容
din=new DataInputStream(s.getInputStream());//返回套接字收到的內容到din中.....