【问题标题】:Speex echo cancellation configurationSpeex 回声消除配置
【发布时间】:2012-09-26 17:30:21
【问题描述】:

我正在使用其 AudioRecord 和 AudioTrack 类以及通过 NDK 的 Speex 来制作一个 Android 到 Android VoIP(扬声器)应用程序来进行回声消除。我能够成功地传入 Speex 的 speex_echo_cancellation() 函数并从中检索数据,但回声仍然存在。

这是录制/发送和接收/播放音频的相关android线程代码:

//constructor
public MyThread(DatagramSocket socket, int frameSize, int filterLength){
  this.socket = socket;
  nativeMethod_initEchoState(frameSize, filterLength);
}

public void run(){

  short[] audioShorts, recvShorts, recordedShorts, filteredShorts;
  byte[] audioBytes, recvBytes;
  int shortsRead;
  DatagramPacket packet;

  //initialize recorder and player
  int samplingRate = 8000;
  int managerBufferSize = 2000;
  AudioTrack player = new AudioTrack(AudioManager.STREAM_MUSIC, samplingRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, managerBufferSize, AudioTrack.MODE_STREAM);
  recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, samplingRate, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, managerBufferSize);
  recorder.startRecording();
  player.play();

  //record first packet
  audioShorts = new short[1000];
  shortsRead = recorder.read(audioShorts, 0, audioShorts.length);

  //convert shorts to bytes to send
  audioBytes = new byte[shortsRead*2];
  ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(audioShorts);

  //send bytes
  packet = new DatagramPacket(audioBytes, audioBytes.length);
  socket.send(packet);

  while (!this.isInterrupted()){

    //recieve packet/bytes (received audio data should have echo cancelled already)
    recvBytes = new byte[2000];
    packet = new DatagramPacket(recvBytes, recvBytes.length);
    socket.receive(packet);

    //convert bytes to shorts
    recvShorts = new short[packet.getLength()/2];
    ByteBuffer.wrap(packet.getData(), 0, packet.getLength()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(recvShorts);

    //play shorts
    player.write(recvShorts, 0, recvShorts.length);

    //record shorts
    recordedShorts = new short[1000];
    shortsRead = recorder.read(recordedShorts, 0, recordedShorts.length);

    //send played and recorded shorts into speex, 
    //returning audio data with the echo removed
    filteredShorts = nativeMethod_speexEchoCancel(recordedShorts, recvShorts);

    //convert filtered shorts to bytes
    audioBytes = new byte[shortsRead*2];
    ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(filteredShorts);

    //send off bytes
    packet = new DatagramPacket(audioBytes, audioBytes.length);
    socket.send(packet);                

  }//end of while loop 

}

这里是相关的 NDK/JNI 代码:

void nativeMethod_initEchoState(JNIEnv *env, jobject jobj, jint frameSize, jint filterLength){
  echo_state = speex_echo_state_init(frameSize, filterLength);
}

jshortArray nativeMethod_speexEchoCancel(JNIEnv *env, jobject jObj, jshortArray input_frame, jshortArray echo_frame){

  //create native shorts from java shorts
  jshort *native_input_frame = (*env)->GetShortArrayElements(env, input_frame, NULL);
  jshort *native_echo_frame = (*env)->GetShortArrayElements(env, echo_frame, NULL);

  //allocate memory for output data
  jint length = (*env)->GetArrayLength(env, input_frame);
  jshortArray temp = (*env)->NewShortArray(env, length);
  jshort *native_output_frame = (*env)->GetShortArrayElements(env, temp, 0);

  //call echo cancellation
  speex_echo_cancellation(echo_state, native_input_frame, native_echo_frame, native_output_frame);

  //convert native output to java layer output
  jshortArray output_shorts = (*env)->NewShortArray(env, length);
  (*env)->SetShortArrayRegion(env, output_shorts, 0, length, native_output_frame);

  //cleanup and return
  (*env)->ReleaseShortArrayElements(env, input_frame, native_input_frame, 0);
  (*env)->ReleaseShortArrayElements(env, echo_frame, native_echo_frame, 0);
  (*env)->ReleaseShortArrayElements(env, temp, native_output_frame, 0);
  return output_shorts;
}

这些代码运行良好,音频数据肯定是从 android 到 android 发送/接收/处理/播放的。给定 8000 Hz 的音频采样率和 2000 字节/1000shorts 的数据包大小,我发现需要 1000 的 frameSize 才能使播放的音频流畅。 filterLength 的大部分值(根据 Speex 文档又名尾长)都会运行,但似乎对回声消除没有影响。

有没有人足够了解 AEC,可以为我提供一些关于实施或配置 Speex 的指示?感谢阅读。

【问题讨论】:

  • 我也有类似的问题。您的问题有解决方案吗?
  • 您好,您找到解决问题的方法了吗?谢谢

标签: android voip speex aec


【解决方案1】:

您是否正确对齐了远端信号(称为recv)和近端信号(称为记录)?总是有一些需要考虑的播放/记录延迟。这通常需要将远端信号在环形缓冲区中缓冲一段时间。在 PC 上,这通常约为 50 - 120 毫秒。在Android上,我怀疑它要高得多。大概在 150 - 400 毫秒的范围内。我建议使用 speex 的 100 毫秒尾长并调整远端缓冲区的大小,直到 AEC 收敛。这些更改应该允许 AEC 收敛,而与包含预处理器无关,这在此处不是必需的。

【讨论】:

    【解决方案2】:

    您的代码是正确的,但在本机代码中缺少某些内容,我修改了 init 方法并在回声消除后添加了 speex 预处理,然后您的代码运行良好(我在 windows 中尝试过) 这是本机代码

    #include <jni.h>
    #include "speex/speex_echo.h"
    #include "speex/speex_preprocess.h"
    #include "EchoCanceller_jniHeader.h"
    SpeexEchoState *st;
    SpeexPreprocessState *den;
    
    JNIEXPORT void JNICALL Java_speex_EchoCanceller_open
      (JNIEnv *env, jobject jObj, jint jSampleRate, jint jBufSize, jint jTotalSize)
    {
         //init
         int sampleRate=jSampleRate;
         st = speex_echo_state_init(jBufSize, jTotalSize);
         den = speex_preprocess_state_init(jBufSize, sampleRate);
         speex_echo_ctl(st, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
         speex_preprocess_ctl(den, SPEEX_PREPROCESS_SET_ECHO_STATE, st);
    }
    
    JNIEXPORT jshortArray JNICALL Java_speex_EchoCanceller_process
      (JNIEnv * env, jobject jObj, jshortArray input_frame, jshortArray echo_frame)
    {
      //create native shorts from java shorts
      jshort *native_input_frame = (*env)->GetShortArrayElements(env, input_frame, NULL);
      jshort *native_echo_frame = (*env)->GetShortArrayElements(env, echo_frame, NULL);
    
      //allocate memory for output data
      jint length = (*env)->GetArrayLength(env, input_frame);
      jshortArray temp = (*env)->NewShortArray(env, length);
      jshort *native_output_frame = (*env)->GetShortArrayElements(env, temp, 0);
    
      //call echo cancellation
      speex_echo_cancellation(st, native_input_frame, native_echo_frame, native_output_frame);
      //preprocess output frame
      speex_preprocess_run(den, native_output_frame);
    
      //convert native output to java layer output
      jshortArray output_shorts = (*env)->NewShortArray(env, length);
      (*env)->SetShortArrayRegion(env, output_shorts, 0, length, native_output_frame);
    
      //cleanup and return
      (*env)->ReleaseShortArrayElements(env, input_frame, native_input_frame, 0);
      (*env)->ReleaseShortArrayElements(env, echo_frame, native_echo_frame, 0);
      (*env)->ReleaseShortArrayElements(env, temp, native_output_frame, 0);
    
      return output_shorts;   
    }
    
    JNIEXPORT void JNICALL Java_speex_EchoCanceller_close
      (JNIEnv *env, jobject jObj)
    {
         //close
         speex_echo_state_destroy(st);
         speex_preprocess_state_destroy(den);
    }
    

    您可以在 speex 库的源 (http://www.speex.org/downloads/) 中找到有用的示例,例如编码、解码、回声消除

    【讨论】:

    • 错误:EchoCanceller_jniHeader.h:没有这样的文件或目录
    猜你喜欢
    • 2012-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多