微信Speex转wav

807 阅读7分钟
原文链接: raye.wang

前言

微信公众号开发,因为需要在页面发送语音和播放,由于公众号页面中录音必须要调用微信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  

会生成 wangrayespeex_SpeexUtil.h文件,这就是C语言的头文件,里面定义了我们SpeexUtil定义的decode,当然是没有实现的,具体实现代码需要我们自己实现

wangrayespeex_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修改后,就可以实现wangrayespeexSpeexUtil.h的方法了,新建wangrayespeexSpeexUtil.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和wangrayespeex_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文件也麻烦告知一下,现在就在头疼这个问题