1. 程式人生 > >Android 平臺語音通話及迴音消除、噪音消除研究

Android 平臺語音通話及迴音消除、噪音消除研究

一 Android平臺語音通訊

正因為Android平臺優越的效能、美觀的介面,越來越多人使用Android手機,從而在Android平臺上的
語音通話越來越多。語音通話大概流程如下:我認為一個語音通話系統至少有四個模組。分別是PCM(Pulse
Code Modulation,即 脈碼編碼調製)語音採集,編解碼,網路傳輸以及語音播放。如果算上UI互動的話,
就是五個模組了。整體流程大概是:A打電話給B,A聲音通過MIC被採集成PCM原始資料,然後經過編碼壓縮,
再通過網路(建立P2P連線)將編碼後的資料傳輸出去;B端通過網路收到資料後進行解碼處理,然後呼叫播
放模組,進行播放資料。如果想通話音質提供些,可以在編碼前加入 噪音消除,迴音消除。

二 錄音、放音、編碼、解碼、網路傳送、接收

 1、語音採集模組
 Android平臺上的實現是通過AudioRecord介面來實現PCM資料的採集,這一步比較容易的。但需要注意的是
AudioRecord介面的使用方法。構造AudioRecord 例項需要引數 public AudioRecord (int audioSource, int
sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)
比如錄音程式碼如下:

    static final int frequency = 8000;   
    static
final int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO; static final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT; int recBufSize,playBufSize; AudioRecord audioRecord; recBufSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); audioRecord = new
AudioRecord(MediaRecorder.AudioSource.MIC, frequency, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT, recBufSize);
2、語音播放

  當語音資料採集好了之後,接著可以實現語音播放模組。Android上實現PCM資料的播放也很簡單,直接
使用AudioTrack這個介面就行了。同樣需要注意該介面
的使用方法。AudioTrack的構造方式跟AudioRecord是對應的

    static final int frequency = 8000;   
    static final int channelConfiguration = AudioFormat.CHANNEL_CONFIGURATION_MONO;     
    static final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;    
    int recBufSize,playBufSize; 
    AudioTrack  audioPlayer ;

  playBufSize =  AudioTrack.getMinBufferSize(frequency,     
             channelConfiguration, audioEncoding);        
  audioPlayer = new AudioTrack(AudioManager.STREAM_MUSIC,frequency,AudioFormat.CHANNEL_OUT_MONO,
  AudioFormat.ENCODING_PCM_16BIT,playBufSize,
               AudioTrack.MODE_STREAM) ;
3、語音編解碼

  採集到的PCM資料是原始的語音資料,如果我們直接進行網路傳輸,那是不可取的。因此,要進行打包編碼。
編碼我們需要第三方的庫,目前我使用的庫是speex(http://www.speex.org)。我看到許多SIP語音電話都
使用到了這個庫進行編解碼。當然也有對這個庫評 價不好的說法,但我覺得作為學習還是可取的,因為speex
使用起來很方便。把speex原始碼下載下來,寫好JNI介面,在NDK環境編譯一下,即可在java環境呼叫。
例如下面一個介面函式

 jint 
 Java_com_audiocodec_talkdemo_AudioCodec_InitAudioEncodec( JNIEnv* env,
                                                 jobject thiz,jint sampling_rate,jint audioLevel)
  {
  if(nInitAudioCodecEncodeFlag == 1 || audioLevel < 3 || audioLevel > 8 )
  return 0 ;

int  frame_size ;

if(sampling_rate == 8000)
{
audio_Leval = 0 ;          
  capAudioLength = 160     ; 
  capAudioBitrate = 8000  ; 
}else if(sampling_rate == 16000)
{
     audio_Leval = 1 ;  
     capAudioLength = 320     ;  
     capAudioBitrate = 16000  ;  
}else if(sampling_rate == 32000)
{
     audio_Leval = 2 ; 
     capAudioLength = 640     ;  
     capAudioBitrate = 32000  ;  
   }else
    return 0 ; 

tmp_Level = audioLevel ; //設定等級 15kbit/s
speex_mode = speex_lib_get_mode(audio_Leval) ; 


enc_state = speex_encoder_init(speex_mode);
speex_encoder_ctl(enc_state,SPEEX_SET_QUALITY,&tmp_Level);

int tmp = 30 ;//丟包補償
int nRet = speex_encoder_ctl(enc_state, SPEEX_SET_PLC_TUNING, &tmp);
nRet = speex_encoder_ctl(enc_state, SPEEX_GET_PLC_TUNING, &tmp);

speex_bits_init(&bits); 

   nInitAudioCodecEncodeFlag = 1 ;

   return 1 ;
 }

//編碼音訊資料
/*
引數
    jbyteArray  szAudio   等待編碼的音訊資料
    jbyteArray  szOut     編碼後的音訊資料
返回值
    成功返回 編碼後長度
    失敗返回 0 
*/

jint Java_com_audiocodec_talkdemo_AudioCodec_AudioEncode( JNIEnv* env,
                            jobject thiz,jbyteArray szAudio,jbyteArray szOut)
{
if(nInitAudioCodecEncodeFlag == 0)
  return 0 ;

  jbyte* szAudioBuffer =  (jbyte *)(*env)->GetByteArrayElements(env,szAudio, 0);
  jbyte* szOutBuffer   =  (jbyte *)(*env)->GetByteArrayElements(env,szOut, 0);

//清空bits ,以便編碼
  speex_bits_reset(&bits);

    //進行編碼
  int nRet = speex_encode_int(enc_state,(spx_int16_t*)szAudioBuffer, &bits);

  //把編碼後的bits 結構,拷貝到cbits_enc的資料可以從網路傳送出去,長度為nByte_enc
  int nByte_enc = speex_bits_write(&bits, szOutBuffer, 200);

  (*env)->ReleaseByteArrayElements(env,szAudio,szAudioBuffer,0) ;
  (*env)->ReleaseByteArrayElements(env,szOut,szOutBuffer,0) ;

  return nByte_enc ;
}                                                 

/*
函式功能 初始化編碼器
引數
    無引數
返回值
    成功返回 1
    失敗返回 0 
*/
jint 
Java_com_audiocodec_talkdemo_AudioCodec_ExitAudioEncodec( JNIEnv* env,
                                                 jobject thiz)
{
if(nInitAudioCodecEncodeFlag == 1)
{
nInitAudioCodecEncodeFlag = 0 ;

//銷燬資源
speex_bits_destroy(&bits); 
speex_encoder_destroy(enc_state);
enc_state = NULL ;
}else
return 0 ;
}
4 網路傳送、接收
   //定義
DatagramSocket udpSocket  ;  

//生成
      try {
udpSocket = new  DatagramSocket(6789);
  } catch (SocketException e1) {
   e1.printStackTrace();
 }

 //傳送
   try {
udpSocket.send(sendPacket) ;
 } catch (IOException e) {
e.printStackTrace();
 }

  //接收
  udpSocket.receive(udpPackage); 

  //關閉
  udpSocket.close() ;

三、 迴音消除

    從Speex 的介紹可以看出它提供了噪音消除,迴音消除,測試比較過噪音消除這功能效果是非
 常棒的,迴音消除這功能也很不錯這一功能,現在開源的,比較完善的迴音消除模組就是Speex了
 ,有許多中小公司也拿它作為迴音消除功能 。經過測試,Speex的消除效果還是不錯的。
 編寫個jni檔案,NDK 環境編譯一下即可得到so 檔案,在Android環境中呼叫即可。
      //初始化迴音消除引數
      /*
       * jint frame_size        幀長      一般都是  80,160,320
       * jint filter_length     尾長      一般都是  80*25 ,160*25 ,320*25
       * jint sampling_rate     取樣頻率  一般都是  8000,16000,32000
       * 比如初始化 
       *  InitAudioAEC(80, 80*25,8000)   //8K,10毫秒取樣一次
       *  InitAudioAEC(160,160*25,16000) //16K,10毫秒取樣一次
       *  InitAudioAEC(320,320*25,32000) //32K,10毫秒取樣一次
       */
jint Java_com_audioaec_talkdemo_AudioAEC_InitAudioAEC( JNIEnv* env,jobject thiz,
              jint frame_size,jint filter_length,jint sampling_rate)
{
if(nInitSuccessFlag == 1)
return 1 ;

m_nFrameSize  = frame_size; 
m_nFilterLen  = filter_length; 
m_nSampleRate = sampling_rate; 

//計算取樣時長,即是10毫秒,還是20毫秒,還是30毫秒
nSampleTimeLong = (frame_size / (sampling_rate / 100)) * 10 ;

m_pState = speex_echo_state_init(m_nFrameSize, m_nFilterLen); 
if(m_pState == NULL)
return -1 ;

m_pPreprocessorState = speex_preprocess_state_init(m_nFrameSize, m_nSampleRate); 
if(m_pPreprocessorState == NULL)
return -2 ;

iArg = m_nSampleRate;
speex_echo_ctl(m_pState, SPEEX_SET_SAMPLING_RATE, &iArg);
speex_preprocess_ctl(m_pPreprocessorState, SPEEX_PREPROCESS_SET_ECHO_STATE, m_pState);

 nInitSuccessFlag = 1 ;

 return 1 ;
}

/*
 引數:
  jbyteArray recordArray  錄音資料
  jbyteArray playArray    放音資料 
  jbyteArray szOutArray
*/
jint Java_com_audioaec_talkdemo_AudioAEC_AudioAECProc(JNIEnv* env,jobject thiz,
           jbyteArray recordArray,jbyteArray playArray,jbyteArray szOutArray  )
{
if(nInitSuccessFlag == 0)
return 0 ;

 jbyte* recordBuffer = (jbyte *)(*env)->GetByteArrayElements(env,recordArray, 0);
 jbyte* playBuffer = (jbyte *)(*env)->GetByteArrayElements(env,playArray, 0);
 jbyte* szOutBuffer = (jbyte *)(*env)->GetByteArrayElements(env,szOutArray, 0);

speex_echo_cancellation(m_pState,(spx_int16_t *)recordBuffer,
       (spx_int16_t *)playBuffer,(spx_int16_t *)szOutBuffer); 
int flag=speex_preprocess_run(m_pPreprocessorState,(spx_int16_t *)szOutBuffer);  

 (*env)->ReleaseByteArrayElements(env,recordArray,recordBuffer,0) ;
 (*env)->ReleaseByteArrayElements(env,playArray,playBuffer,0) ;
 (*env)->ReleaseByteArrayElements(env,szOutArray,szOutBuffer,0) ;

 return 1 ;
}

    //退出
jint Java_com_sosea_xmeeting_SpeexAEC_ExitSpeexDsp( JNIEnv* env,jobject thiz)
{
if(nInitSuccessFlag == 0)
return 0 ;

if (m_pState != NULL) 
{ 
speex_echo_state_destroy(m_pState); 
m_pState = NULL; 
} 
if (m_pPreprocessorState != NULL) 
{ 
speex_preprocess_state_destroy(m_pPreprocessorState); 
m_pPreprocessorState = NULL; 
} 

 nInitSuccessFlag = 0 ;

 return 1 ;
}

四 、 噪音消除處理

// 初始化 降噪
Java_com_audioaec_talkdemo_AudioAEC_InitAudioDeNose( JNIEnv* env,
                                                 jobject thiz)
{
 int denoise_enabled = 1 ;
if(nInitDeNoseFlag == 1)
return 0 ;

 nInitDeNoseFlag = 1 ;

 //8K降噪
audioProcNose8K = speex_preprocess_state_init(80 * (nSampleTimeLong / 10),8000);  
speex_preprocess_ctl(audioProcNose8K, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled);

//16K降噪
audioProcNose16K = speex_preprocess_state_init(160 * (nSampleTimeLong / 10),16000);  
speex_preprocess_ctl(audioProcNose16K, SPEEX_PREPROCESS_SET_DENOISE, &denoise_enabled);

 return 1 ;
}

//8K降噪 
jint Java_com_audioaec_talkdemo_AudioAEC_AudioDeNose8K(JNIEnv* env,jobject thiz,jbyteArray recordArray)
{
if(nInitDeNoseFlag == 0)
return 0 ;

  jbyte* recordBuffer = (jbyte *)(*env)->GetByteArrayElements(env,recordArray, 0);

  speex_preprocess(audioProcNose8K,(spx_int16_t*)recordBuffer, NULL);

 (*env)->ReleaseByteArrayElements(env,recordArray,recordBuffer,0) ;

 return 1 ;
}

//16K降噪 
jint Java_com_audioaec_talkdemo_AudioAEC_AudioDeNose16K(JNIEnv* env,jobject thiz,jbyteArray recordArray)
{
if(nInitDeNoseFlag == 0)
return 0 ;

  jbyte* recordBuffer = (jbyte *)(*env)->GetByteArrayElements(env,recordArray, 0);

  speex_preprocess(audioProcNose16K,(spx_int16_t*)recordBuffer, NULL);

 (*env)->ReleaseByteArrayElements(env,recordArray,recordBuffer,0) ;

 return 1 ;
}

// 釋放降噪
jint 
Java_com_audioaec_talkdemo_AudioAEC_ExitAudioDeNose( JNIEnv* env,
                                                 jobject thiz)
{
if(nInitDeNoseFlag == 0)
return 0 ;

 nInitDeNoseFlag = 0 ;

speex_preprocess_state_destroy(audioProcNose8K); 
speex_preprocess_state_destroy(audioProcNose16K); 

 return 1 ;
}