Speex语音压缩与解压在Android上的实现

    由于项目需要做语音降噪处理,最近做了这方面的研究。但结果却没有达到,反而却学得了基于Speex的语音压缩和解压,也算没有白白浪费时间(300K的语音文件经过Speex压缩后文件大小变为了30K左右,对于网络传输非常好)。

    关于Speex:http://www.speex.org/

    Speex主要提供的技术:

                               1.窄带(8kHz),宽带(16kHz)和超宽带(32kHz)压缩于同一位流

                               2.强化立体编码
                               3.数据包丢失隐蔽
                               4.可变比特率(VBR)
                               5.语音捕捉(VAD)
                               6.非连续传输(DTX)
                               7.定点运算
                               8.感官回声消除(AEC)
                               9.噪音屏蔽

    首先参考了一位大神的博客 speex编解码在android上实现,由于博客比较久了,现在按照上面的步骤去做,结果却出现些小小的Bug。其中主要的是在Android.mk文件中的LOCAL_SRC_FILES所对应的.c文件,最新的Speex版本,已经去掉了些libspeex文件夹下的.c文件,参照Speex中的libspeex文件进行对比,删除多余.c文件。以下为去掉之后需要的.c文件(加上自己的speex.cpp)。

LOCAL_SRC_FILES := \
libspeex/bits.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/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/kiss_fft.c \
libspeex/kiss_fftr.c \
libspeex/lpc.c \
libspeex/lsp.c \
libspeex/lsp_tables_nb.c \
libspeex/ltp.c \
libspeex/modes.c \
libspeex/modes_wb.c \
libspeex/nb_celp.c \
libspeex/quant_lsp.c \
libspeex/sb_celp.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

     配置好Android.mk文件后,其次主要是如何调用Speex所提供的方法。对于语音的压缩和解压,Speex主要提供了open(),getFrameSize(),decode(),encode(),close()方法。其中open()方法中需要传一个压缩质量(quality是一个0~10(包含10)范围内的整数)的参数;getFrameSize()方法获得帧字节的大小;encode()方法中有short lin[], int offset, byte encoded[], int size四个参数,其中short lin[]表示录音得到的short型数据,int offset为跳过的字节数,byte encoded[]为压缩后的byte型数据,int size为数据的长度;decode()方法中有byte encoded[], short lin[], int size三个参数,其中byte encoded[]表示压缩后的byte型数据, short lin[]为解压后的short型数据,int size为数据大小;close()方法退出语音压缩和解压。

   Sppex.cpp文件的代码主要为:


#include <jni.h>

#include <string.h>
#include <unistd.h>

#include <include/speex/speex.h>

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;

static JavaVM *gJavaVM;

extern "C" JNIEXPORT jint JNICALL Java_com_example_speex_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);
	tmp = compression;
	speex_encoder_ctl(enc_state, SPEEX_SET_QUALITY, &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);

	return (jint) 0;
}

extern "C" JNIEXPORT jint Java_com_example_speex_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);

	for (i = 0; i < nsamples; i++) {
		env->GetShortArrayRegion(lin, offset + i * enc_frame_size,
				enc_frame_size, buffer);
		speex_encode_int(enc_state, buffer, &ebits);
	}
	//env->GetShortArrayRegion(lin, offset, enc_frame_size, 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;
}

extern "C" JNIEXPORT jint JNICALL Java_com_example_speex_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;
}

extern "C" JNIEXPORT jint JNICALL Java_com_example_speex_Speex_getFrameSize(
		JNIEnv *env, jobject obj) {

	if (!codec_open)
		return 0;
	return (jint) enc_frame_size;

}

extern "C" JNIEXPORT void JNICALL Java_com_example_speex_Speex_close(
		JNIEnv *env, jobject obj) {

	if (--codec_open != 0)
		return;

	speex_bits_destroy(&ebits);
	speex_bits_destroy(&dbits);
	speex_decoder_destroy(dec_state);
	speex_encoder_destroy(enc_state);
} 

    编写好Sppex.cpp后,接下来是在Java中调用这些方法,下面为录音和播放:

    1.录音

      private List<Data> mDatas = new ArrayList<Data>();

    private static final class Data {
        private int mSize;//记录每次编码之后的大小,用做解码时设置每次读取的字节大小
        private byte[] mBuffer;//记录每次编码之后的字节数组,可以用做边录边播放
    }

    Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);//设置线程的优先级,音频优先级最高
                new Thread(new Runnable() {
                    public void run() {
                        try {
                            mAudioRecord.startRecording();
                            DataOutputStream dos = new DataOutputStream(
                                    new BufferedOutputStream(
                                            new FileOutputStream(mAudioFile)));
                            int sizeInShorts = speex.getFrameSize();
                            short[] audioData = new short[sizeInShorts];
                            int sizeInBytes = speex.getFrameSize();

                            //开始记录数据

                            while (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                                int number = mAudioRecord.read(audioData, 0,
                                        sizeInShorts);
                                short[] dst = new short[sizeInBytes];
                                System.arraycopy(audioData, 0, dst, 0, number);
                                byte[] encoded = new byte[sizeInBytes];
                                int count = speex.encode(dst, 0, encoded,
                                        number);
                                if (count > 0) {

                                    //记录每次录音得到的数据,与播放录音的时候对应

                                    Data data = new Data();
                                    data.mSize = count;
                                    data.mBuffer = encoded;
                                    mDatas.add(data);
                                    dos.write(encoded, 0, count);
                                }
                            }
                            dos.flush();
                            dos.close();
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();



   

   

    2.播放

         new Thread(new Runnable() {
                    public void run() {
                        try {
                            DataInputStream dis = new DataInputStream(
                                    new BufferedInputStream(
                                            new FileInputStream(mAudioFile)));
                            int len = 0;

                            //根据每次录音得到的数据,进行播放

                            for (Data data : mDatas) {
                                byte[] encoded = new byte[data.mSize];
                                len = dis.read(encoded, 0, data.mSize);
                                if (len != -1) {
                                    short[] lin = new short[speex
                                            .getFrameSize()];
                                    int size = speex.decode(encoded, lin,
                                            encoded.length);
                                    if (size > 0) {
                                        mAudioTrack.write(lin, 0, size);
                                        mAudioTrack.play();
                                    }
                                }
                            }

                            //直接使用存放录音时的数据播放,如果不存入文件,可边录边放

                            // short[] lin = new short[speex.getFrameSize()];
                            // for (Data data : mDatas) {
                            // int size = speex.decode(data.mBuffer, lin,
                            // data.mSize);
                            // if (size > 0) {
                            // mAudioTrack.write(lin, 0, size);
                            // mAudioTrack.play();
                            // }
                            // }
                            dis.close();
                            speex.close();
                            mAudioTrack.stop();
                        } catch (FileNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();

    到这儿,基于Speex的语音压缩和解压基本实现。 参考了网上很多的资料,有的实现了,但却经过了OGG封装(较复杂),有点不懂为什么需要OGG封装,下来再研究下。另外,Speex也提供了Java语言的实现,请参考JSpeex。


    Speex Demo下载地址:基于Speex的语音压缩和解压在Android中的实现

    在后来继续研究发现,对于语音的压缩、解压、噪音抑制、回声消除,及时通信等,WebRTC技术完全能够满足,并且特别强大,该技术被Google收购了,当然Android中也提供了实现,但是是在Android4.1版本之后,而且并不是所有的Android手机都支持,所以最好还是和Speex一样用JNI调用C/C++代码。之后将写博客仔细剖解。可参考例子:Android 4.1回声消除(AcousticEchoCanceler)和噪声抑制 (NoiseSuppressor)Demo

    

 

 

 

你可能感兴趣的:(android,解决方案,语音,speex)