微信Speex转wav,Speex to wav

前言

微信公众号开发,因为需要在页面发送语音和播放,由于公众号页面中录音必须要调用微信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 <jni.h>
/* 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 <memory.h>
#include <stdio.h>
#include <malloc.h>
#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 <memory.h>
#include <stdio.h>
#include <malloc.h>
#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文件也麻烦告知一下,现在就在头疼这个问题

展示评论