由于项目需要做语音降噪处理,最近做了这方面的研究。但结果却没有达到,反而却学得了基于Speex的语音压缩和解压,也算没有白白浪费时间(300K的语音文件经过Speex压缩后文件大小变为了30K左右,对于网络传输非常好)。
关于Speex:http://www.speex.org/
Speex主要提供的技术:
1.窄带(8kHz),宽带(16kHz)和超宽带(32kHz)压缩于同一位流
首先参考了一位大神的博客 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