speex演算法在android上的移植
l最近在調speex介面引數,將speex演算法的一些特性給新增進去,比如:降噪,靜音檢測,白噪聲新增,增益等等。下面我們就先簡單介紹一些spexx算
法。speex語音演算法主要是針對VOIP應用的一個開源演算法,他集合了多種功能,除了如上所述的,還增加了回聲消除(ACE)等功能,能夠在多種平臺
進行應用。下面我主要介紹一下speex在android平臺上的應用。
首先我們來介紹一下speex演算法的模組劃分。在介紹之前,我們最好去speex官網 http://www.speex.org/downloads/ 去下載他的相關文件以及原始碼。其
中有一個包speex-api-reference.tar.gz 就有speex模組的相關介紹。其API介紹在1.2為止,speex總共分為以下9大模組:
--Speex encoder and decoder。——編碼和解碼模組。
--SpeexBits:Bit-stream mainpulations。——位元流操作模組,也就是資料的讀寫模組。
--Various definitions for Speex callbacks supported by the decoder。——解碼回撥模組。
--SpeexEchoState:Acoustic echo caceller。——回聲消除模組。
--SpeexHeader:Makes it easy to writ/parse an Ogg/Speex header。——ogg格式相關的處理模組。
--JitterBuffer:Adaptive jitter buffer。——語音抖動緩衝模組。
--SpeexJitter:Adaptive jitter buffer specifically for Speex。——針對speex演算法特點優化的語言抖動處理模組。
--SpeexPreprocessState:The Speex preprocessor。——Speex其他相關特點的處理模組,如:降噪,靜音檢測等。
--SpeexStereoState:Handing Speex stereo files。——立體聲處理的相關模組。
.以上就是Speex演算法的主要模組,每個模組都有相關功能的函式介面,具體我們可以去檢視其api的相關介紹。
好了現在我們來介紹其在android平臺的使用。由於其使用的是C實現的,所以要想在android進行呼叫其相關方法就必須通過JNI的方法進行呼叫,所以
我們首先就必須獲得speex演算法的一個.so檔案,因此我們先使用cygwin編譯獲取.so檔案。
一、將speex相關原始碼複製進專案
下載speex原始碼,在專案中新建資料夾,命名為jni。將speex原始碼下的include,libspeex兩個檔案的原始碼複製進jni資料夾中。將include資料夾下的
speex_config_types.h.in檔案改為speex_config_types.h檔案,並且將其中的內容改為以下內容:
#ifndef _SPEEX_CONFIG_TYPES_H
#define _SPEEX_CONFIG_TYPES_H
typedef signed short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef signed int spx_int32_t;
typedef unsigned int spx_uint32_t;
#endif /* _SPEEX_CONFIG_TYPES_H */
二、編寫Android.mk以及Application.mk相關檔案。
Android.mk檔案內容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libspeex
LOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_H
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_SRC_FILES := speex_jni.cpp \
./libspeex/bits.c \
./libspeex/buffer.c \
./libspeex/cb_search.c \
./libspeex/exc_10_16_table.c \
./libspeex/exc_10_32_table.c \
./libspeex/exc_20_32_table.c \
./libspeex/exc_5_256_table.c \
./libspeex/exc_5_64_table.c \
./libspeex/exc_8_128_table.c \
./libspeex/fftwrap.c \
./libspeex/filterbank.c \
./libspeex/filters.c \
./libspeex/gain_table.c \
./libspeex/gain_table_lbr.c \
./libspeex/hexc_10_32_table.c \
./libspeex/hexc_table.c \
./libspeex/high_lsp_tables.c \
./libspeex/jitter.c \
./libspeex/kiss_fft.c \
./libspeex/kiss_fftr.c \
./libspeex/lpc.c \
./libspeex/lsp.c \
./libspeex/lsp_tables_nb.c \
./libspeex/ltp.c \
./libspeex/mdf.c \
./libspeex/modes.c \
./libspeex/modes_wb.c \
./libspeex/nb_celp.c \
./libspeex/preprocess.c \
./libspeex/quant_lsp.c \
./libspeex/resample.c \
./libspeex/sb_celp.c \
./libspeex/scal.c \
./libspeex/smallft.c \
./libspeex/speex.c \
./libspeex/speex_callbacks.c \
./libspeex/speex_header.c \
./libspeex/stereo.c \
./libspeex/vbr.c \
./libspeex/vq.c \
./libspeex/window.c
include $(BUILD_SHARED_LIBRARY)
具體每行的含義可以參看我之前的一篇部落格,或者自行搜尋Android.mk的編寫方法。
Applicatio.mk 內容如下:
APP_ABI := armeabi armeabi-v7a
三、編寫本地方法介面檔案speex
public native int open(int compression);
public native int getFrameSize();
public native int decode(byte encoded[], short lin[], int size);
public native int encode(short lin[], int offset, byte encoded[], int size);
public native void close();
四、使用java當中的javah工具編譯這個jni介面檔案。
使用cmd進入到專案bin/classes目錄下,輸入以下命令:javah -jni xxx.xxx.xxx.speex。前面的xxx為speex檔案的包名。編譯完成後會在classes檔案下看到
一個com_poctalk_codec_Speex.h檔案,將這個檔案複製進jni目錄下。
五、編寫speex.cpp檔案
#include <jni.h>
#include "com_poctalk_codec_Speex.h"
#include <string.h>
#include <unistd.h>
#include <speex/speex.h>
#include <speex/speex_preprocess.h>
#include <speex/speex_echo.h>
#pragma comment(lib,"libspeexdsp.lib")
static int codec_open = 0;
static int dec_frame_size;
static int enc_frame_size;
static SpeexBits ebits, dbits;
void *enc_state;
void *dec_state;
SpeexPreprocessState *preprocess_state;
//SpeexEchoState *echo_state;
static JavaVM *gJavaVM;
extern "C"{
JNIEXPORT jint JNICALL Java_com_poctalk_codec_Speex_open(JNIEnv *env, jobject obj, jint compression) {
int tmp;
if (codec_open++ != 0)
return (jint)0;
speex_bits_init(&ebits);
speex_bits_init(&dbits);
//設定編碼為窄帶編碼
enc_state = speex_encoder_init(&speex_nb_mode);
dec_state = speex_decoder_init(&speex_nb_mode);
//設定編碼為寬頻編碼
//enc_state = speex_encoder_init(&speex_wb_mode);
//dec_state = speex_decoder_init(&speex_wb_mode);
tmp = compression;
speex_encoder_ctl(enc_state, SPEEX_SET_QUALITY, &tmp);//設定編碼的位元率,即語音質量。由引數tmp控制
speex_encoder_ctl(enc_state, SPEEX_GET_FRAME_SIZE, &enc_frame_size);
speex_decoder_ctl(dec_state, SPEEX_GET_FRAME_SIZE, &dec_frame_size);
preprocess_state =speex_preprocess_state_init(160, 8000);//建立預處理物件
//echo_state = speex_echo_state_init(160, 5000);//建立回聲消除物件
//int sampleRate = 8000;
//speex_echo_ctl(echo_state, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
int denoise = 1;
int noiseSuppress = -25;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_DENOISE, &denoise); //降噪
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_NOISE_SUPPRESS, &noiseSuppress); //設定噪聲的dB
int agc = 1;
float q=24000;
//actually default is 8000(0,32768),here make it louder for voice is not loudy enough by default. 8000
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_AGC, &agc);//增益
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_AGC_LEVEL,&q);
int vad = 1;
int vadProbStart = 80;
int vadProbContinue = 65;
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_VAD, &vad); //靜音檢測
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_PROB_START , &vadProbStart); //Set probability required for the VAD to go from silence to voice
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_PROB_CONTINUE, &vadProbContinue); //Set probability required for the VAD to stay in the voice state (integer percent)
return (jint)0;
}
JNIEXPORT jint JNICALL Java_com_poctalk_codec_Speex_encode
(JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) {
jshort buffer[enc_frame_size];
jbyte output_buffer[enc_frame_size];
int nsamples = (size-1)/enc_frame_size + 1;
int i, tot_bytes = 0;
if (!codec_open)
return 0;
speex_bits_reset(&ebits);//在每幀輸入之前將所有的編碼狀態重置
speex_echo_state_reset(echo_state);//
for (i = 0; i < nsamples; i++) {
env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer);
//input_frame麥克風採集到的資料,Echo_Data是從speaker處獲取到的資料,out_frame為回聲消除後的資料
//speex_echo_cancellation(echo_state,input_frame,Echo_Data,out_frame);//回聲消除
speex_preprocess_run(preprocess_state, buffer);
speex_encode_int(enc_state, buffer, &ebits);
}
tot_bytes = speex_bits_write(&ebits, (char *)output_buffer,enc_frame_size);//返回實際被寫入的位元組數
env->SetByteArrayRegion(encoded, 0, tot_bytes,output_buffer);
return (jint)tot_bytes;
}
JNIEXPORT jint JNICALL Java_com_poctalk_codec_Speex_decode
(JNIEnv *env, jobject obj, jbyteArray encoded, jshortArray lin, jint size) {
jbyte buffer[dec_frame_size];
jshort output_buffer[dec_frame_size];
jsize encoded_length = size;
if (!codec_open)
return 0;
env->GetByteArrayRegion(encoded, 0, encoded_length, buffer);
speex_bits_read_from(&dbits, (char *)buffer, encoded_length);
speex_decode_int(dec_state, &dbits, output_buffer);
env->SetShortArrayRegion(lin, 0, dec_frame_size,
output_buffer);
return (jint)dec_frame_size;
}
JNIEXPORT jint JNICALL Java_com_poctalk_codec_Speex_getFrameSize(JNIEnv *env, jobject obj) {
if (!codec_open)
return 0;
return (jint)enc_frame_size;
}
JNIEXPORT void JNICALL Java_com_poctalk_codec_Speex_close(JNIEnv *env, jobject obj) {
if (--codec_open != 0){
return;
}
//speex_echo_state_destroy(echo_state);//
speex_preprocess_state_destroy(preprocess_state);
speex_bits_destroy(&ebits);
speex_bits_destroy(&dbits);
speex_decoder_destroy(dec_state);
speex_encoder_destroy(enc_state);
}
}
六、使用cygwin對整個專案進行編譯。
編譯完成後,refresh專案會在libs目錄下生成兩個資料夾armeabi,armeabi-v7a 其中分別有一個libspeex.so檔案。
至此.so檔案的編譯已經完成了,我們就可以在專案中對本地方法進行呼叫,去進行語音的編解碼。由於使用方面我已經在專案中進行應用了,所以就不
掛出來了,不過我的語言模組也是參考網上的一個專案進行編寫的,名字叫做android-recorder-6.0,你可以下載他的原始碼進行模仿。不過還有一點需要
說明的是在編寫.cpp檔案時,我沒有將回聲消除的功能給加進去,在回聲消除的這個問題上浪費了我很多時間,剛開始沒有看他的api,不知道回聲消除
是哪個模組實現的,不知道該怎樣使用回聲消除的api,後來看了api,又不知道怎樣在呼叫回聲消除的函式時,該怎樣傳遞引數進去,後來問同事知道,
回聲消除的功能是針對全雙工的通訊方式,也就是喇叭和錄音模組都開啟,如果是半雙工的通訊方式,比如:手持機,回聲消除的功能其實可有可無。
但是既然提到了又浪費了很多時間,那就不妨講一講回聲消除功能的呼叫。
首先我們在預處理時,就應該回聲消除的預處理:
//echo_state = speex_echo_state_init(160, 5000);//建立回聲消除物件
//int sampleRate = 8000;
//speex_echo_ctl(echo_state, SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate);
sampleRate就是我們設定好的錄音採用頻率。
然後在語音編碼的時候,進行回聲消除功能的呼叫:
//input_frame麥克風採集到的資料,Echo_Data是從speaker處獲取到的資料,out_frame為回聲消除後的資料
//speex_echo_cancellation(echo_state,input_frame,Echo_Data,out_frame);//回聲消除
上面speex_echo_cancellation函式的三個引數一次為,回聲消除物件,inpt_frame為喇叭播放資料,Echo_Data為從麥克風獲取的資料,out_frame為最後
回聲消除後的資料。可能有人會對這幾個引數比較迷惑,那是因為不瞭解回聲的產生原因。由於是全雙工通訊,當我們在錄音的時候,也可能在進行聲音
播放,這樣就會導致有時候錄音也會將喇叭正在播放的聲音給錄進去,這樣就產生了回聲的效果,所以第二個引數才要將播放的資料作為引數傳遞進去。
在編譯.so檔案的過程中,我還遇到了這樣一個問題:multiple definition 。後來才發現在我的android.mk檔案中將speex_jni.cpp引用了兩次。
參考部落格: