網絡多媒體是目前Web應用的一個發展方向。在網頁上錄音,並發給朋友,相信他們收到你的聲音一定非常高興。如今這已經是一項很普通的多媒體應用技術,筆者使用Java語言開發出了能夠嵌入Web頁面上的Applet錄音機(界面外觀如下圖所示)。如果你有編程的興趣,不妨試試。在此,我將這種技術介紹給各位編程愛好者。
多媒體基本概念及常識
開發多媒體音頻軟件,必須了解一些多媒體基本概念,諸如采樣、量化、量化位、采樣頻率、單雙聲道、音頻編解碼、音頻壓縮格式等概念。采樣是把時間上連續的模擬信號變成時間上離散的、有限個樣值的信號。量化是在幅度上把連續值的模擬信號變為離散值的數字信號。在時間軸上已變為離散的樣值脈沖,在幅度軸上仍會在動態范圍內有連續值,可能出現任意幅值,即在幅度軸上仍是模擬信號的性質,因此必須用有限個電平等級來代表實際量值。量化位是每個采樣點能夠表示的數據范圍,經常采用的有8、12和16位。采樣頻率是將模擬聲音波形轉換為數字時,每秒鐘所抽取聲波幅度樣本的次數,采樣頻率的計算單位是Hz(赫茲)。根據采樣理論,為了保證聲音不失真,采樣頻率應為聲音頻率的兩倍左右。記錄聲音時,如果每次生成一個聲波數據,稱為單聲道;每次生成二個聲波數據,成為立體聲(雙聲道)。量化位和采樣頻率越高,音質就越好。
正常人耳聽覺的聲音頻率范圍大約在20Hz至20KHz之間,人的語音頻率大概在300Hz至3.4KHz之間。對於語音來說,采用8KHz的采樣頻率已經足夠了。所以我們采用8KHz采樣頻率、16位量化位、單聲道來記錄和播放語音,就可以滿足網頁上的語音需求了。
網頁錄音機的制作過程
了解了以上常識,下面來看看錄音機的制作過程。
首先要碰到的是音頻采集。音頻采集有很多種,JDK 1.3中構建TargetDataLine類實例來實現聲音數據采集。在此例中,我采用Visual J++的J/Direct調用Windows API函數來實現聲音的采集。
聲音俘獲主要由AudioCapture、AudioDataEvent、AudioDataListener三個類組成。我們依次按照0.1秒時間采樣數據塊的大小(即1600字節)作為所送出到系統的錄音數據緩沖區大小,這樣也就相當於每隔0.1秒聲卡提交給我們一次聲音數據。另外,使用Windows API方式聲音采集有一個好處,錄音過程不需要建立單獨的線程。這是因為有回調函數的巨大作用,這樣節省了系統資源,提高了程序的穩定性。
//聲音俘獲類:
...
public class AudioCapture{
...
public AudioCapture(){
...
waveincaps=new WAVEINCAPS();
wavehdr=new WAVEHDR[bufferlen];
}
public void addAudioDataListener(AudioDataListener lter){
listeners.addElement(lter);
}
void applyBuffer(){
for(int i=0;i<bufferlen;i++){
wavehdr[i]=new WAVEHDR();
int
adr=dlllib.addrOfPinnedObject(dlllib.getPinnedHandle(data[i]));
wavehdr[i].lpData=adr;
...
}
}
int chkData(byte[] a){//將整型低16位高低位交叉並轉為字節
...
}
public void close(){
if(!useful)return;
isclose=true;
waveInStop(deviceid[0]);
...
}
...
synchronized void notifyListener(int minValue,byte[] audioData){
AudioDataEvent evt=new AudioDataEvent(this,minValue,audioData);
For(Enumeration enu=listeners.elements();enu.hasMoreElements();)
((AudioDataListener)enu.nextElement()).onAudioDataArrived(evt);
}
...
public void setMuteValue(int muteValue){
this.muteValue=muteValue;
}
/**@dll.import("WINMM",auto)*/
...
public static native int waveInReset(int hwi);
private class c extends Callback{
AudioCapture record;
c(AudioCapture tt){
record=tt;
}
...
}
}
//緩沖數據提交事件類:
import java.util.EventObject;
public class AudioDataEvent extends EventObject{
...
}
//事件偵聽接口
import java.util.EventListener;
public interface AudioDataListener extends EventListener{
public abstract void onAudioDataArrived(AudioDataEvent evt);
}
第二步,數據的編碼壓縮存儲。由聲卡采集的數據是一連串16位脈沖編碼調制(PCM格式)的數據,數據量很大,如果不采取壓縮處理,不利於文件的存儲和傳輸。所以要進行數據的壓縮編碼,這就是我們會碰到的聲音編碼數據格式。壓縮編碼方法有很多種,有GSM、IAM4、AU格式編碼等,這些壓縮算法比較簡單,可以在很多網站上獲得壓縮和解壓縮的源代碼。筆者對這幾種壓縮格式進行了測試,其主要參數及品質對比見上表。
壓縮格式主要參數及品質對比
壓縮格式 GSM IMA4 AU
壓縮比 10:1 4:1 2:1
文件大小 很小 小 中
聲音質量 一般 好 好
編解碼速度 慢 快 很快
數據量(byte/s) 165 400 800
一般來說,人說話時並不是非常連續的,哪怕是你有意發連續的聲音,其實有很多時間段是處在靜音狀態(沒有聲音或聲音很小,量化數據值很小),只要記錄它的一個狀態就可以了。所以聲音數據區的數據格式就是(數據頭+數據體)的方式。對於靜音數據,數據頭為0,數據體為空。
在聲音回放時,先讀數據頭,如果數據頭不為0,則解壓數據體播放;如果數據頭為0,則暫停一定時間或者寫入一定長度的靜音數據即可。采用這種方式可以大大減小記錄語音文件,並且不影響聲音的還原回放。
//處理錄音數據到達事件
public void onAudioDataArrived(AudioDataEvent evt){
...
int min=evt.getAudioMinValue();
fileWriter.write((min==0?0:1));
if(min!=0){
Convert.BytesToInts(evt.getAudioData(),audiodata);
fileWriter.write(Codec.encode(evt.getAudioData()));
}
...
}
第三步,聲音采集編碼保存結束後,就可以回放我們錄制的聲音。
接下來我們要在網頁上來播放它。Java Applet支持AU格式聲音的回放,使用非常簡單。所以我們把錄制並壓縮的聲音數據解碼為AU格式,就可以很方便地進行聲音回放了。在sun.audio包中提供的au流數據播放sun.audio.AudioPlayer.start(InputStream),實際上是虛擬機的au播放類每隔50毫秒依次調用InputStream的read(byte[],abyte0,int i,int j)方法,每次讀取長度為400字節的AU格式數據用來播放。我們知道,輸入流的read方法是阻塞方式的,而解壓縮聲音數據是要費時的,如果在其請求數據時再解壓數據並寫入緩沖區,則播放聲音時聽起來會斷斷續續的,那是不可行的。所以需要單獨建立解壓縮數據的線程,也就是說從虛擬機的au播放類讀取數據的線程中獨立出來,用緩沖區做為兩個線程的管道連接,解壓縮線程不停地寫入緩沖區中,播放線程不斷地從緩沖區中讀取並播放。這樣一來,對於播放聲音的暫停、停止等功能就很容易實現。所以我們需要重載read(byte[],abyte0,int I,int j)方法。
//音頻緩沖區類
import java.io.*;
public class AudioBuffer extends InputStream{
int capacity=4096;
...
public AudioBuffer(AudioPlayer ap){
player=ap;
clearData();
}
public void close(){
isclose=true;
synchronized(putManager){
if(waitingPut>0)putManager.notify();
}
synchronized(getManager){
if(waitingGet>0)getManager.notify();
}
}
public void clearData(){
...
}
public void suspend(){ispause=true;}
public void resume(){ispause=false;}
public void write(int i){
synchronized(putManager){
while(emptyBytes<1){
waitingPut++;
try{
putManager.wait();
}catch(InterruptedException _ex){}
waitingPut--;
}
...
}
synchronized(getManager){
usedBytes++;
if(waitingGet>0)getManager.notify();
}
}
public void write(byte[] d){
write(d,0,d.length);
}
public void write(byte[] d,int i,int j){
...
synchronized(getManager){
usedBytes+=j;
if(waitingGet>0)getManager.notify();
}
}
public int read(){
...
}
public int read(byte[] d){
return read(d,0,d.length);
}
public int read(byte[] d,int i,int j){
if(isclose)return -1;
if(ispause){
d[i]=127;
return 1;
}
synchronized(getManager){
...
}
System.arraycopy(data,readPtr,d,i,k);
ReadPtr+=k;
readPtr%=capacity;
usedBytes-=j;
}
synchronized(putManager){
emptyBytes+=j;
if(waitingPut>0)putManager.notify();
}
try{
player.notifyListener(AudioPlayEvent.PLAY_DATA,d);
}catch(Exception e){}
return j;
}
}
//音頻播放器類
public class AudioPlayer implements Runnable{
...
AudioBuffer buffer=new AudioBuffer(this);
sun.audio.AudioPlayer.start(buffer);
...
byte[] auData=new byte[400];
//存放解壓後的au數據
int compressLength=165;
//GSM格式為165;IMA格式為400;AU格式為800
byte[] compressData=new byte[compressLength];
//存儲從文件流中讀取的壓縮格式數據
...
public void run(){
...
fileInputStream.read(compressData);//從音頻文件讀取壓縮格式數據,此輸入流要處理靜音
Codec.decode(abyte0,auData); //解壓縮數據到auData
Buffer.write(auData,0,400);//寫入到緩沖區
...
}
...
}
最後,將上述各個模塊拼接起來,並把GUI做好就可以使用了。如果在GUI上加入聲音顯示效果,你的錄音機就更酷了。
總結
這裡介紹的聲音處理的主要技術,可以組合或衍生使用。例如,可以在網頁上方便地實現聲音回放;可以加入調節播放起點(跳躍播放)功能,而不使用其它播放器;還可以做語音留言系統。如果數據通過流格式以組播方式實時傳出,另一端接收下來然後播放出來,又可非常簡單地實現實時語音系統,即語音聊天。另外,有了自己的語音庫,還可以實現簡單的語音合成技術,讓計算機讀出文本內容。