前言
微信公众号开发,因为需要在页面发送语音和播放,由于公众号页面中录音必须要调用微信js录音,录音完成由前端上传到微信临时素材,再由后端下载到服务器,然后给前端播放,但是因为从微信下载下来的语音智能是speex格式(高清语音)和amr格式,然而这2种格式都是无法直接在HTML中播放的,所以需要对语音进行转码,由于speex格式清晰度较高,所以我选择了下载speex格式的语音进行转码,本文就是记录如果一步一步调用speex官方源码和微信提供部分C代码进行转码,注:本文所有环境和命令是基于Linux的
下载并安装speex
环境:
Linux Centos
Gcc
JDK 1.8
speex 1.2.0
步骤:
首先下载speex最新的源码,下载地址,解压然后进入源码目录,执行命令
sudo ./configure
验证环境是否有误,如果有问题,则根据具体提示自行安装和配置,如果没有异常,则可以执行命令进行编译安装了
sudo make;sudo make install
如果没有出问题,则会在/usr/local/lib文件夹下面产生libspeex.so等文件,如果有问题,则根据具体提示解决,因为我这里没有遇到任何问题,所以也无法提供常见的问题了
编写Java调用C语言代码
在Java中调用C或者C++的代码技术叫做JNI,是Java原生支持的,首先我们要定义好原生方法的定义,代码如下
package wang.raye.speex;
import java.lang.reflect.Field;
/**
* Speex 转码工具类
* @author Raye
* @since 2017年10月19日17:04:47
*/
public class SpeexUtil {
/**
* .speex to .wav
* @param in .speex文件路径
* @param out .wav文件路径
* @return
*/
public static native boolean decode(String in, String out);
static {
try{
System.load(System.getProperty("user.dir")+java.io.File.separator+"libjspeex.so");
} catch (Exception e) {
e.printStackTrace();
}
}
}
其中
public static native boolean decode(String in, String out);
是定义的原生方法,也就是C语言的方法,而static部分是加载C语言代码的动态库,有2种方法可以加载动态链接库
System.load
和
System.loadLibrary
其中System.load 参数必须为库文件的绝对路径,可以是任意路径,System.loadLibrary 参数为库文件名,不包含库文件的扩展名,但是库路径必须是在JVM属性java.library.path所指向的路径中,这里我是获取的绝对路径,就是项目目录,因为用的spring boot直接打包的jar运行的,类写好之后生成class文件,然后用Javah命令生成C语言的.h文件,在class文件执行命令
javah -classpath . wang.raye.speex.SpeexUtil
会生成
wang_raye_speex_SpeexUtil.h文件,这就是C语言的头文件,里面定义了我们SpeexUtil定义的decode,当然是没有实现的,具体实现代码需要我们自己实现
wang_raye_speex_SpeexUtil.h 内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class wang_raye_speex_SpeexUtil */
#ifndef _Included_wang_raye_speex_SpeexUtil
#define _Included_wang_raye_speex_SpeexUtil
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: wang_raye_speex_SpeexUtil
* Method: decode
* Signature: (Ljava/lang/String;Ljava/lang/String;)Z
*/
JNIEXPORT jboolean JNICALL Java_wang_raye_speex_SpeexUtil_decode
(JNIEnv *, jclass, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
修改微信demo
具体方法实现可以调用微信的demo,首先下载微信的demo,下载地址
由于微信demo里面是main方法的,所以需要进行修改,原本的SpeexDecode.c代码
#include
#include
#include
#include "TRSpeex.h"
int main(int argc, char* argv[])
{
FILE *fpInput;
FILE *fpOutput;
char aInputBuffer[MAX_FRAME_SIZE*10];
char aOutputBuffer[MAX_FRAME_SIZE*10];
int ret;
int buffer_size;
int nOutSize;
int nPackNo;
TRSpeexDecodeContex SpeexDecode;
int nTotalLen;
char buf[44];
if(argc <3)
{
printf("Usage SpeexDecode InputspxFile OutputWavFile\n");
return 1;
}
memset(aInputBuffer,0,sizeof(char)*MAX_FRAME_SIZE*10);
memset(buf,0,44);
buf[0] = 'R';
buf[1] = 'I';
buf[2] = 'F';
buf[3] = 'F';
buf[8] = 'W';
buf[9] = 'A';
buf[10] = 'V';
buf[11] = 'E';
buf[12] = 'f';
buf[13] = 'm';
buf[14] = 't';
buf[15] = 0x20;
buf[16] = 0x10;
buf[20] = 0x01;
buf[22] = 0x01;
buf[24] = 0x80;
buf[25] = 0x3E;
buf[29]= 0x7D;
buf[32] = 0x02;
buf[34] = 0x10;
buf[36] = 'd';
buf[37] = 'a';
buf[38] = 't';
buf[39] = 'a';
TRSpeexDecodeInit(&SpeexDecode);
fpInput = fopen(argv[1],"rb");
if(fpInput == NULL)
{
printf("can't open input spx file");
return 0;
}
fpOutput = fopen(argv[2],"wb");
if(fpOutput == NULL)
{
printf("can't open output file");
return 0;
}
fwrite(buf,1,44,fpOutput);
nTotalLen = 0;
buffer_size = 6;
ret = fread(aInputBuffer, 1,buffer_size,fpInput);
while(1)
{
TRSpeexDecode(&SpeexDecode,aInputBuffer,buffer_size,aOutputBuffer, &nOutSize);
ret = fread(aInputBuffer, 1,buffer_size, fpInput);
if(ret != buffer_size)
break;
fwrite(aOutputBuffer,1, nOutSize,fpOutput);
nTotalLen += nOutSize;
}
TRSpeexDecodeRelease(&SpeexDecode);
fseek(fpOutput,40,SEEK_SET);
fwrite(&nTotalLen,1,4,fpOutput);
fseek(fpOutput,4,SEEK_SET);
nTotalLen += 36;
fwrite(&nTotalLen,1,4,fpOutput);
fclose(fpOutput);
fclose(fpInput);
return 0;
}
首先需要把方法名称由main方法改为自己想要的名字,这里我改成了decode,其次修改参数,因为Java调用传递的是2个字符串参数,speex的路径和转码后的wav的路径,所以需要先将原来的参数argc删除,并删除
if(argc <3)
{
printf("Usage SpeexDecode InputspxFile OutputWavFile\n");
return 1;
}
同时删除argv参数,添加两个参数char* in ,char* out,分别对应speex的路径和转码后的wav的路径,然后修改代码中的
fpInput = fopen(argv[1],"rb");
为
fpInput = fopen(in,"rb");
修改
fpOutput = fopen(argv[2],"wb");
为
fpOutput = fopen(out,"wb");
修改后的代码为
#include
#include
#include
#include "TRSpeex.h"
int decode(char* in,char* out)
{
FILE *fpInput;
FILE *fpOutput;
char aInputBuffer[MAX_FRAME_SIZE*10];
char aOutputBuffer[MAX_FRAME_SIZE*10];
int ret;
int buffer_size;
int nOutSize;
int nPackNo;
TRSpeexDecodeContex SpeexDecode;
int nTotalLen;
char buf[44];
memset(aInputBuffer,0,sizeof(char)*MAX_FRAME_SIZE*10);
memset(buf,0,44);
buf[0] = 'R';
buf[1] = 'I';
buf[2] = 'F';
buf[3] = 'F';
buf[8] = 'W';
buf[9] = 'A';
buf[10] = 'V';
buf[11] = 'E';
buf[12] = 'f';
buf[13] = 'm';
buf[14] = 't';
buf[15] = 0x20;
buf[16] = 0x10;
buf[20] = 0x01;
buf[22] = 0x01;
buf[24] = 0x80;
buf[25] = 0x3E;
buf[29]= 0x7D;
buf[32] = 0x02;
buf[34] = 0x10;
buf[36] = 'd';
buf[37] = 'a';
buf[38] = 't';
buf[39] = 'a';
TRSpeexDecodeInit(&SpeexDecode);
fpInput = fopen(in,"rb");
if(fpInput == NULL)
{
printf("can't open input spx file");
return 0;
}
fpOutput = fopen(out,"wb");
if(fpOutput == NULL)
{
printf("can't open output file");
return 0;
}
fwrite(buf,1,44,fpOutput);
nTotalLen = 0;
buffer_size = 6;
ret = fread(aInputBuffer, 1,buffer_size,fpInput);
while(1)
{
TRSpeexDecode(&SpeexDecode,aInputBuffer,buffer_size,aOutputBuffer, &nOutSize);
ret = fread(aInputBuffer, 1,buffer_size, fpInput);
if(ret != buffer_size)
break;
fwrite(aOutputBuffer,1, nOutSize,fpOutput);
nTotalLen += nOutSize;
}
TRSpeexDecodeRelease(&SpeexDecode);
fseek(fpOutput,40,SEEK_SET);
fwrite(&nTotalLen,1,4,fpOutput);
fseek(fpOutput,4,SEEK_SET);
nTotalLen += 36;
fwrite(&nTotalLen,1,4,fpOutput);
fclose(fpOutput);
fclose(fpInput);
return 0;
}
修改完成后为了方便引用改文件后缀c为h
实现原生方法
微信demo修改后,就可以实现wang_raye_speex_SpeexUtil.h的方法了,新建wang_raye_speex_SpeexUtil.c,编写如下代码
#include "wang_raye_speex_SpeexUtil.h"
#include "SpeexDecode.h"
JNIEXPORT jboolean JNICALL Java_wang_raye_speex_SpeexUtil_decode
(JNIEnv * env, jclass p2, jstring p3, jstring p4)
{
const char *str3 = (*env)->GetStringUTFChars(env, p3, 0);
const char *str4 = (*env)->GetStringUTFChars(env, p4, 0);
return 0==decode(str3,str4);
}
这里就是实现了一个调用中转,在这个方法中调用刚刚修改的SpeexDecode.h的方法
打包so
代码写完后,需要把wangrayespeexSpeexUtil.h和wang_raye_speex_SpeexUtil.c以及修改过的微信demo的代码进行打包成so文件。注:Windows环境下就是DLL文件
首先创建打包文件makefile-linux,编写一下内容
#共享库文件名,lib*.so
TARGET := libjspeex.so
#compile and lib parameter
#编译参数
CC := gcc
LIBS :=-lspeex
LDFLAGS :=
DEFINES :=
INCLUDE := -I. -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux
CFLAGS := -g -Wall -O3 $(DEFINES) $(INCLUDE)
CXXFLAGS:= $(CFLAGS) -DHAVE_CONFIG_H
SHARE := -fPIC -shared -o
#i think you should do anything here
#下面的基本上不需要做任何改动了
#source file
#源文件,自动找所有.c和.cpp文件,并将目标定义为同名.o文件
SOURCE := $(wildcard *.c) $(wildcard *.cpp)
OBJS := $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCE)))
.PHONY : everything objs clean veryclean rebuild
everything : $(TARGET)
all : $(TARGET)
objs : $(OBJS)
rebuild: veryclean everything
clean :
rm -fr *.o
rm -rf *.so
veryclean : clean
rm -fr $(TARGET)
$(TARGET) : $(OBJS)
$(CC) $(CXXFLAGS) $(SHARE) $@ $(OBJS) $(LDFLAGS) $(LIBS)
rm -rf *.o
install:
rm -rf /usr/local/lib/$(TARGET)
cp $(TARGET) /usr/local/lib
其中libjspeex是动态库的名字,保存后执行命令
sudo make -f makefile-linux
如果没有异常则执行命令
sudo make -f makefile-linux install
完成后会在/usr/local/lib文件夹中生成libjspeex.so文件,如果编译时出现
relocation R_X86_64_32 against `.rodata' can not be used when making a shared object
是由于系统是AMD64位的,所以需要在编译的时候添加 -fPIC 选项,需要修改makefile-linux的CC := gcc行为CC := gcc -fPIC
再重新执行命令即可
使用
将生成的libjspeex.so放到项目根目录即可使用,如果使用时提示
speex.xxxx --cannot open shared object file: No such file or directory,
则是因为/usr/local/lib并没有在系统的环境变量里面,可以修改/etc/ld.so.conf,然后刷新
vi /etc/ld.so.conf
增加一行 include /usr/local/lib
sudo ldconfig
结尾
本文只是记录了我在使用过程中遇到的一些问题,有些没有遇到的欢迎补充,另外如果知道Java如果加载jar中so文件也麻烦告知一下,现在就在头疼这个问题