1. 程式人生 > >speex演算法在android上的移植

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引用了兩次。

參考部落格: