1. 程式人生 > >【轉】用Java實現網路語音訊號傳送

【轉】用Java實現網路語音訊號傳送

本文轉載自部落格:https://www.aliyun.com/jiaocheng/347518.html

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

一、引言 

  Java是一門適合於分散式計算環境、尤其是Internet程式設計的語言。這不僅僅在於java具有很好的安全性和可移植性,還在於java為Internet程式設計提供了豐富的網路類庫的支援。利用這些網路類庫,可以輕鬆編寫多種型別的網路通訊程式。然而由於某些限制,Java在傳輸多媒體資訊方面的應用不是很廣,大部分的應用都集中在網路上傳輸語音 
等音訊訊號的方面。傳輸音訊訊號應用方案一般有兩種,一是應用於資料廣播的多對一傳輸,例如音訊資料伺服器向數個客戶端傳送音訊資料訊號,其最廣泛的應用則是某些網上的IP電話,大家經常可以看到不少這種提供線上IP電話服務的網站的客戶端都是使用的嵌在網頁上的Java Applet程式,用來實現撥號、通話等等基本的網路電話功能; 第二種方案則是我們今天要涉及的部分,一對一的音訊訊號資料的傳輸。這種方案的應用範圍更廣。大家都去過語音聊天室,大部分的語音聊天室的語音聊天功能的實現就是使用的Java技術,大家對這樣網頁的原始碼分析一下就可以發現這一點。

  我曾開發一個專案,涉及使用java來實現在網路上傳輸語音資料。開發中遇到不少問題,而且在網際網路上發現關於java語音傳輸的資料比較少,尋找了許多天,最終從一個開放原始碼的一個簡單的Answer Machine 演示程式中獲得瞭解決問題的方法。今天我就把我在點對點傳輸音訊訊號方面的一些經驗拿出來,與大家共同探討這方面的問題。

  二、存在的問題

  在網路上傳輸音訊的方面存在的問題主要可以歸納為以下幾點: 

  1 雙方之間的網路連線

  要進行頻資料的傳輸,首先就是要建立資料連結。常用的通訊協議中,TCP較可靠,所以用在不允許資料丟失的應用上。而UDP則較多應用於處理速度要求較快、資料傳輸可靠性要求不是很高的應用上,如資料廣播。通訊協議的選擇取決於我們所要做的應用的型別。怎樣建立網路連線,穩定的接收和傳送音訊訊號的資料流是關鍵。

  2 音訊訊號的採集以及回放

  在進行音訊訊號的採集中我們必須考慮到取樣率的問題,聲音訊號的取樣率有8Khz、16Khz、32Khz、44Khz等,每種資料取樣慮產生的資料量都不一樣,越高的取樣率產生的資料量越大,所以我們要選擇合適的取樣率以適應網路的頻寬。

  3 音訊數字訊號的編碼與解碼。

  如果把直接採集到的音訊訊號資料流在網路上進行傳輸,它所佔有的頻寬也是十分大的,以8Khz的取樣率採集14位的音訊資料那麼就有以下這樣的一個式子: 

14 bit * 8000/second=112,000 bits/second or112kbps 

  從中我們可以看出以這樣的方式傳輸音訊資料,每秒需要向網路中傳送112kb的資料。所以。從節省頻寬的角度考慮,我們很有必要對這樣的資料進行壓縮。對多媒體訊號的壓縮我們有許多可以選擇的格式,如mp2、mp3、GSM等等。同樣,我們這裡也存在一個對壓縮格式進行選擇的問題,考慮到音訊資料傳輸的及時性,對傳輸的音訊資料質量的要求,以及各種壓縮格式的壓縮比率以及進行壓縮和解壓縮所要耗費的系統資源等方面問題,選擇合適的壓縮格式就顯得尤為重要。

三、解決的方法

  下面就針對前面提出的問題討論一下解決的辦法。

  1 雙方之間的網路連線

  Java在這方面有其獨特的優勢,Java提供了豐富的網路類庫的支援,可以輕鬆編寫多種型別的網路通訊程式。在我下面的例子中我就使用了TCP/IP協議,通過Java的Socket類進行程式設計。

  2 音訊訊號 
的採集和回放以及音訊數字訊號的編碼與解碼

  在解決這兩個問題的時候,在網上很幸運地通過一些文章的介紹,找到了Answer Machine 演示程式的原始碼(由of jsresources.org的Florian Bomers 和Matthias Pfisterer編寫,網址http://www.jsresources.org/apps/am.html)。在這個程式程式碼中,有幾個解決我們問題所需要的類,而且作者將這些類封裝的很好,我們基本不需要做什麼改動,只需要遮蔽其中的除錯資訊的輸出就行了,更可貴的是它還封裝了幾種常見的音訊格式。其中的GSM格式(Global System for Mobile Telecommunications)就是我們下面例子中採用的壓縮格式,GSM格式可以將128kbps 的音訊資料流 (16bit通過8k Hz的音訊取樣) 壓縮為13kbps 的音訊資料流,非常適合語音訊號的傳送,所以可謂是一石二鳥。

  我分析過這幾個類的原始碼,不得不佩服它的作者,每個類的原始碼都很精煉,大家可以自己分析一下。好了下面就給大家講講這幾個類,並且將它們用到的Java Sound API中的類和函式等一併做個簡單介紹,讓大家對Java Sound API中常用的類也有個大致的瞭解。由於Java Sound API中的類比較多。限於篇幅無法對所有用到的類做詳盡的解釋,以下內容只是簡單提及了各個類的用途和使用規範,有關Java Sound API中類的具體介紹請大家訪問這裡http://java.sun.com/j2se/1.4.2/docs/api/, 查詢javax.sound.sampled的相關內容。

  以下的提到幾個檔案是從Answer Machine 演示程式的原始碼中提取出來的,由於是開放原始碼的程式,大家在使用的時候請注意相關的公共協議。

  ① AMAudioFormat類(封裝在AMAudioFormat.java檔案中)

  AMAudioFormat類封裝了CD、FM、TELEPHONE、GSM這四種質量的音訊格式的引數,使用起來也非常簡單,這樣我們在使用Java Sound API時就不用自己去寫那些複雜的程式碼了,但為了明白Java Sound API的原理,我們需要對它的程式碼做一下分析。它使用了Java Sound API中的AudioFormat這個類,這個類非常重要,在Java中對任何音訊資料的使用都要實現通過它指定所需要使用的音訊格式,AudioFormat類有一個巢狀的類AudioFormat.Encoding,實際上大部分對AudioFormat類的使用都是使用的這個巢狀的類。

  AMAudioFormat類的重要方法: 

  名稱:getLineAudioFormat
  呼叫格式:getLineAudioFormat(整型音訊格式代號)
  返回值: 根據傳遞音訊格式代號生成的AudioFormat物件。

  說道這裡大家可能要問了,那麼通過Java Sound API可以直接使用GSM格式嗎?答案是比較複雜,但同樣有解決的辦法,作者在這裡使用了另外的開源程式的類庫-tritonus的GSM編碼解碼庫。大家需要在這裡www.tritonus.org/plugins.html下載tritonous_share.jar和tritonus_gsm.jar兩個檔案,並在AMAudioFormat類中引用,這樣就完成了GSM格式的設定。需要告訴大家的是在對AMAudioFormat.java這個類進行編譯後,我們的程式執行的時候就可以不需要tritonous_share.jar和tritonus_gsm.jar這兩個檔案的支援了。

  ② AudioCapture類(封裝在AudioCapture.java檔案中)

  AudioCapture類封裝了從音訊硬體捕獲音訊資料並自動編碼為GSM音訊壓縮資料的過程,並且通過它的getAudioInputStream()方法提供給我們一個音訊資料輸入流,我們就可以直接將這個流傳送到網路中。

  AudioCapture 類的重要方法: 
  名稱:getAudioInputStream
  呼叫格式:getAudioInputStream()
  返回值:AudioInputStream物件

  AudioCapture 類使用了Java Sound API中的AudioInputStream、AudioFormat、AudioSystem這幾個類和TargetDataLine、LineListener介面。除了AudioFormat類我再簡單介紹一下其他的類: 

  AudioInputStream 類是帶有特殊音訊格式和長度的InputStream類,它有兩個構造方法,分別是AudioInputStream(InputStream stream, AudioFormat format,long length)和AudioInputStream(TargetData -Line line)。

  TargetDataLine 介面是DataLine介面的一種,通過它就可以直接從音訊硬體獲取資料了,它有幾個常用的方法,分別是:open(AudioFormat format)、void open(AudioFormat format, int bufferSize)、int read(byte[] b, int off, int len)。

  AudioSystem 類是Java標準音頻系統的入口點,在AudioSystem 類中使用他的getLine() 方法建立TargetDataLine物件。

  LineListener介面用來對線路狀態改變的時間進行監聽,他的重要的方法是update(LineEvent event)方法。

  ③ AudioPlayStream類(封裝在AudioPlayStream.java檔案中)

  AudioPlayStream類與AudioCapture類剛好相反,它封裝了GSM壓縮音訊資料的解碼和音訊訊號的回放過程,提供給我們一個音訊訊號輸出流。AudioCapture類用到的Java Sound API中的類它也基本都用到了,只是它使用了SourceDataLine介面而不是TargetDataLine介面

  ④ Debug類(封裝在Debug.java檔案中)

  Debug類主要用來在除錯時輸出訊息,程式碼很少,後來我把其中輸出資訊的語句都遮蔽了,對程式執行沒有影響。

  為了方便使用以上的幾個類,我們需要對它們進行編譯和打包,編譯時需要設定相關的編譯環境,以下是我們需要用到的命令列

set CLASSPATH=%CLASSPATH%;.;tritonus_gsm.jar;tritonus_share.jar 
javac am\*.java am\audio\*.java 
jar cmf packaging\manifest.mf am.jar am\*.class 
am\audio\*.class 

  說明一下,我將以上提到的Java原始碼檔案放在了am目錄下,編譯之後可以得到一個8k的am.jar檔案,我們下一步所需要做的就是在我們的程式中引用這個包。

四、例項介紹

  有了以上的基本的介紹,我就可以通過對我寫的一個極為簡單的語音對講軟體程式碼的解釋讓大家更清楚地瞭解一下這幾個模組的具體使用方法,大家可以從中獲得開發具有諸如網路電話、自動應答等功能的軟體的類似方法,用於語音資料的傳輸。

  程式的結構: 


  整個程式分三層,作用分別如下: 

  . 頂層: 使用者介面

  . 中間層: 控制層

  . 底層: 傳輸層

  程式有兩個主要的類: (表)

類名 描述 
CallLink 網路傳輸層,用於接收或傳送音訊資料。 
VoiceSender 作為第二個啟動的執行緒提供從音訊硬體捕獲並編碼好的資料給網路傳輸層。 



  程式的主類jphone使用了Runnable和ActionListener介面,主類除了基本的幾個方法之外,還具有方法initAudioHardware()、ShowMSG、startPhone分別用於初始化AudioCapture類與AudioPlayStream類、顯示程式狀態和開始程式。主類jphone具有兩個子類VoiceSender和CallLink。

  子類VoiceSender同樣使用了Runnable介面,它在程式中作為第二個啟動的執行緒負責傳送捕獲到的音訊資料。CallLink子類就是負責建立scoket連線,並且負責接收或傳送網路資料、監聽網路連線等功能的實現。它具有主要的方法是getInputStream()、getOutputStream()、listen()、open()、close()等。

  為了讓大家更清楚的瞭解程式的結構請大家看下面的類圖。

點選在新視窗中瀏覽此圖片
程式的基本工作流程: 

  當程式啟動時首先執行建立當前主類的例項,當按下呼叫按鈕的時候執行startPhone()方法,startPhone()方法通過呼叫initAudioHardware()方法建立AudioCapture物件和AudioPlayStream物件的例項PhoneMIC和PhoneSPK, 緊接著在建立CallLink子類的例項curCallLink來與具有目標IP地址的計算機進行scoket連線後,startPhone()方法又將子類VoiceSender作為secondThread執行緒啟動,然後又呼叫run()方法。 run()方法通過已經建立的CallLink子類的例項curCallLink監聽網路上的資料(也就是等待別人的呼叫),一旦有音訊資料到來curCallLink 例項就為AudioPlayStream 物件PhoneSPK 提供網路傳來的音訊資料,而PhoneSPK在一個迴圈中不斷的將音訊資料轉換為音訊訊號,完成類似電話聽筒的功能。

  子類VoiceSender 就作為第二執行緒啟動的時候,startPhone() 方法傳遞給它的引數是例項化的CallLink 子類curCallLink , 子類VoiceSender 通過例項化的AudioCapture 物件PhoneMIC 將音訊訊號壓縮成GSM資料,並通過curCallLink 將音訊資料傳送到具有目標IP 地址的計算機上,完成類似電話受話器的功能。

  在這裡例項化的CallLink 子類curCallLink 就相當於兩個電話之間的電話線,這樣通過我以上的解釋大家對程式的原理就有一個大概的瞭解了吧。

  其中的音訊資料傳送執行緒和音訊資料接收執行緒是同步的,不過考慮到網路的因素,可能在聲音的傳輸上有一些延遲,不過由於延遲比較小對及時聽到對方的話語影響不大。
點選在新視窗中瀏覽此圖片

編寫程式碼摘要: 

  在使用AudioCapture 類和AudioPlayStream 類的方法之前需要知道怎樣初始化這兩個類。在宣告AudioCapture 物件的時候需要傳遞給它一個靜態的整型值用於表達將音訊訊號壓縮的方式,這個靜態的整型常量可以是AMAudioFormat 類的以下四個值之一: FORMAT_CODE_CD 、FORMAT_CODE_FM 、FORMAT 
_CODE_TELEPHONE 、FORMAT_CODE_GSM 。所以宣告AudioCapture 物件就要用一下的形式: 


private AudioCapture PhoneMIC null; 
PhoneMIC new AudioCapture(AMAudioFormat. 
FORMAT_CODE_GSM);  


  而宣告AudioPlayStream 物件則不同,我們在初始化它的時候需要傳遞給它一個AudioFormat 物件,用於通知它我們所要播放音訊的格式,這個AudioFormat 物件可以通過AMAudioFormat 類的getLineAudioFormat(格式引數值)方法獲得,其中格式引數的取值和上面提到過的AMAudioFormat 的四個值相同,所以宣告AudioPlayStream 物件就要用以下的形式: 


private AudioPlayStream PhoneSPK null; 
AudioFormat format null; 
format AMAudioFormat.getLineAudioFormat 
(AMAudioFormat.FORMAT_CODE_GSM); 
PhoneSPK new AudioPlayStream(format);  


  在這之後就可以使用AudioCapture 和AudioPlayStream 物件的open() 方法開啟音訊捕獲和音訊回放通道完成它們的初始化了。如以下的形式: 


PhoneMIC.open(); 
PhoneSPK.open();  


  初始化完成之後要使AudioPlayStream 物件播放聲音還需要以下過程,首先建立一個緩衝區(位元組陣列)用於存放從網路傳來的音訊資料流,然後執行AudioPlayStream 物件的start() 方法使AudioPlayStream 
物件開始聲音的回放,這時執行一個while 迴圈,在迴圈中將音訊流資料寫入緩衝區,再使用AudioPlayStream物件的write()方法將緩衝區的資料還原成語音訊號然後播放出來。如下面的例子: 


boolean complete false; 
byte[] gsmdata new byte[bufSize]; 
int numBytesRead 0; 
...... 
PhoneSPK.start(); 
...... 
complete false; 
while((!Thread.currentThread().interrupted()) ) 
{
 try 
 { 
  numBytesReadplaybackInputStream.read(gsmdata);
  if(numBytesRead == -1) 
  { 
   complete=true; 
   break; 
  } 
  PhoneSPK.write(gsmdata, 0, numBytesRead);
 }
 catch (IOException e) 
 {
  System.exit(1); 
 } 
}  


  其中complete 的值用於標誌終止聲音播放的異常原因。

  類似的,初始化完成之後要使AudioCapture 物件捕獲和壓縮聲音資料還需要其他的操作,首先宣告一個InputStream 物件,賦其值為AudioCapture 物件的getAudioInputStream() 方法的返回值,執行
AudioCapture 物件的start() 方法,然後在建立一個迴圈,將通過InputStream 的read() 方法得到的資料傳送到網路上。例如以下程式碼: 


InputStream myIStream null; 
myIStream PhoneMIC.getAudioInputStream(); 
...... 
while((!Thread.currentThread().interrupted())) 
b = myIStream.read(compressedVoice,0, bufSize); 
sendStream.write(compressedVoice,0,b); 
...... 



  通過使用CallLink 的幾個方法,我們可以方便的傳輸和接收音訊資料流。以下是它的程式碼: 


class CallLink 
//使用套接字進行連線
String ipAddr null; 
Socket outSock = null; 
ServerSocket inServSock null; 
Socket inSock null; 
CallLink(String inIP) 
ipAddr inIP; 
void open() throws IOException, UnknownHostException 
{//開啟網路連線 
if (ipAddr != null) 
outSock=new Socket(ipAddr,TALK_PORT); 


void listen() throws IOException 
{// 監聽,等候呼叫
inServSock new ServerSocket(TALK_PORT); 
inSock inServSock.accept(); 
}

public InputStream getInputStream()throws IOException 
{//返回音訊資料輸入流 
if (inSock != null) 
return inSock.getInputStream(); 
else 
return null; 
}
publicOutputStreamgetOutputStream()throwsIOException 
{//返回音訊資料輸出流 
if (outSock != null) 
return outSock.getOutputStream(); 
else 
return null; 
}

void close() throws IOException 
{//關閉網路連線 
inSock.close(); 
outSock.close(); 
}



  程式的程式碼總體有366 行,限於篇幅,這裡就不一一列舉了。

  編譯以及程式的使用方法:

  執行和編譯本程式需要加上額外的環境引數,為了方便使用可以建立以下內容的批處理檔案:(假設程式所需要的包均在程式所在目錄下的lib 資料夾中)

  用於編譯的批處理程式c.bat 的內容

javac -classpath .;lib\am.jar jphone.java 

  用於執行的批處理程式r.bat 的內容

java -classpath .;lib\am.jar jphone 

  啟動時在A 計算機的IP 地址框內輸入要進行連線的計算機B 的IP 地址,在計算機B 的IP 地址框內輸入要進行連線的計算機A 的IP 地址,讓後分別點選“撥出電話”按鈕就可以進行連線了。當然別忘了接上麥克風和開啟音箱電源,呵呵。

  提醒大家,這裡的IP 位址列裡預先存在的地址是127.0.0.1,也就是說,程式也可以和自己進行連線,點選“撥出電話”按鈕,等8 秒左右敲敲你的麥克風,聽到沒有,是不是也有“嘣、嘣、嘣”的聲音?