星期三, 1月 17, 2007

Java 實現語音教學

雖然放寒假了,還是要去實驗室。先學先熟悉?不太知道,不過昨天接獲學長講汪庭安老師想要我們幫忙作一個線上家教系統。

我很想說不要做,因為之前加入過一個台中的線上家教公司,結果,他招不到學生就倒了。所以對於這個系統不是很有信心,聽學長說著說著才知道這是要給家扶中心的小朋友做的系統,經由各大學的慈幼社提供免費的教學,當然這能讓雙方都獲得好處。那這一個系統構想是想要有一個平台能讓老師跟學生不知道對方的真實身分而在線上作教學,也希望有語音功能且可以紀錄師生之間的互動(怕有怪咖想要誘拐小弟弟小妹妹)。



  • 在網路上傳輸音頻的方面存在的問題主要可以歸納為以下幾點:
      1. 雙方之間的網路連接
      要進行頻數據的傳輸,首先就是要建立數據連結。常用的通訊協議中,TCP較可靠,所以用在不允許數據丟失的應用上。而UDP則較多應用於處理速度要求較快、數據傳輸可靠性要求不是很高的應用上,如數據廣播。通信協定的選擇取決於我們所要做的應用的類型。怎樣建立網路連接,穩定的接收和發送音頻信號的數據流是關鍵。
      2. 音頻信號的採集以及回放
      在進行音頻信號的採集中我們必須考慮到采樣率的問題,聲音信號的采樣率有8Khz、16Khz、32Khz、44Khz等,每種數據采樣慮產生的數據量都不一樣,越高的采樣率產生的數據量越大,所以我們要選擇合適的采樣率以適應網路的帶寬。
      3. 音頻數字信號的編碼與解碼。
      如果把直接採集到的音頻信號數據流在網路上進行傳輸,它所佔有的帶寬也是十分大的,以8Khz的采樣率採集14位的音頻數據那麼就有以下這樣的一個式子: 14 bit * 8000/second=112,000 bits/second or112kbps

  • 下面就針對前面提出的問題討論一下解決的辦法。
      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類主要用來在調試時輸出訊息,代碼很少,後來我把其中輸出資訊的語句都屏蔽了,對程式運行沒有影響。

  • 有了以上的基本的介紹,就可以透過一個極為簡單的語音對講軟體代碼的解釋讓大家更清楚地了解一下這幾個模組的具體使用方法,大家可以從中穫得開發具有諸如網路電話、自動應答等功能的軟體的類似方法,用於語音數據的傳輸。
      程式的結構:
      整個程式分三層,作用分別如下:
      . 頂層: 用戶介面
      . 中間層: 控制層
      . 底層: 傳輸層
      程式有兩個主要的類:
    1.CallLink:網路傳輸層,用於接收或發送音頻數據。
    2.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();
    }

  啟動時在A 電腦的IP 地址框內輸入要進行連接的電腦B 的IP 地址,在電腦B 的IP 地址框內輸入要進行連接的電腦A 的IP 地址,讓後分別點擊“撥出電話”按鈕就可以進行連接了。當然別忘了接上麥克風和打開音箱電源,提醒大家,這裡的IP 地址欄裏預先存在的地址是127.0.0.1。

全文



整理完一整篇文章之後,我不禁問自己,我要先進實驗室嗎?還是先多修幾門課好讓我研究所(或許)有機會提早畢業呢?

也想要像許多學長一樣去美國當交換學生。但是每次都定不下心來唸英文。日復一日...想一想之前想要寫的六個人的小世界讀後感也不斷延後。

可能我最先需要唸完的是超效能時間管理,呵呵!

13 則留言:

  1. 請問可以給我關於本則的 source code 嗎? 我對於java applet audio record 有興趣. 可是還在學習中. TMy email :
    ohbelldandy@yahoo.com.tw

    回覆刪除
  2. 你好:
    我想請教一個問題,
    有關網路廣播這類的軟體,
    若利用你所說的網路語音教學方式(44k)
    可以一對多單向傳送嗎?
    (類似media encoder的功能)
    但media encoder delay太久。
    請問java audio stream可以比較快就讓clinet端播放出聲音嗎?
    若方便的話請回信給我,
    dellt2@yahoo.com.tw
    麻煩在主旨請寫出: java audio stream
    我才不會當廣告信刪除
    謝謝

    回覆刪除
  3. 請問可以給我關於本則的 source code 嗎? @ @

    我的信箱

    h24936211@gmail.com

    非常感激您@ @"

    回覆刪除
  4. 我對於Java語音很有興趣,可以將
    本文的source code 寄給我嗎?
    我的信箱k110491@yahoo.com.tw
    謝謝你唷 ^^

    回覆刪除
  5. 你好!!我目前還是學生!!我對語音這方面很有興趣~可以跟您要本則的 source code 嗎? 如果方便的話請Mail給我謝謝!!
    dreamland_760406@hotmail.com

    回覆刪除
  6. 你好, 我目前還是學生,我對語音方面有興趣,想加以研究...想請問你方便把關於本則的source code給我嗎??
    email:
    wonbim@hotmail.com
    謝謝

    回覆刪除
  7. 你好 最近教授出了一個聊天室的作業

    雖然是不需要語音的部分

    但我還是想了解清楚

    我可以要這份講解的SOURCE CODE嗎?

    我對於音頻部分還是希望有CODE來做了解

    我的 GMAIL: ascia1112@gmail.com

    感謝

    回覆刪除
  8. 您好…我對於本則CODE有興趣…可否麻煩您寄給我呢?
    shanaandlai@gmail.com
    真的非常謝謝您…另外想問的是 是否有類似功能的C語言撰寫出來的呢?

    回覆刪除
  9. 您好
    我也可以跟您索取本則的source code嗎?
    fornicator.tw@yahoo.com.tw
    謝謝

    回覆刪除
  10. 可以給我關於本則的 source code 嗎?
    我最近剛好在寫聲音傳輸的部分
    謝謝

    信箱是:
    godmorris9999@gmail.com

    回覆刪除
  11. 我的source已經不知道跑去哪了,可以到http://www.jsresources.org/apps/am.html下載,裡面也有說明和原始碼。

    回覆刪除