微信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最新的源码,下载地址,解压然后进入源码目录,执行命令

COPY
1
sudo ./configure 

验证环境是否有误,如果有问题,则根据具体提示自行安装和配置,如果没有异常,则可以执行命令进行编译安装了

COPY
1
sudo make;sudo make install

如果没有出问题,则会在/usr/local/lib文件夹下面产生libspeex.so等文件,如果有问题,则根据具体提示解决,因为我这里没有遇到任何问题,所以也无法提供常见的问题了

编写Java调用C语言代码

在Java中调用C或者C++的代码技术叫做JNI,是Java原生支持的,首先我们要定义好原生方法的定义,代码如下

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
}
}
}

其中

COPY
1
public static native boolean decode(String in, String out);

是定义的原生方法,也就是C语言的方法,而static部分是加载C语言代码的动态库,有2种方法可以加载动态链接库

COPY
1
System.load

COPY
1
System.loadLibrary

其中System.load 参数必须为库文件的绝对路径,可以是任意路径,System.loadLibrary 参数为库文件名,不包含库文件的扩展名,但是库路径必须是在JVM属性java.library.path所指向的路径中,这里我是获取的绝对路径,就是项目目录,因为用的spring boot直接打包的jar运行的,类写好之后生成class文件,然后用Javah命令生成C语言的.h文件,在class文件执行命令

COPY
1
javah -classpath . wang.raye.speex.SpeexUtil

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

wang_raye_speex_SpeexUtil.h 内容:

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 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代码

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#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删除,并删除

COPY
1
2
3
4
5
6
if(argc <3)
{
printf("Usage SpeexDecode InputspxFile OutputWavFile\n");
return 1;

}

同时删除argv参数,添加两个参数char* in ,char* out,分别对应speex的路径和转码后的wav的路径,然后修改代码中的

COPY
1
fpInput = fopen(argv[1],"rb");

COPY
1
fpInput = fopen(in,"rb");

修改

COPY
1
fpOutput = fopen(argv[2],"wb");

COPY
1
fpOutput = fopen(out,"wb");

修改后的代码为

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#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,编写如下代码

COPY
1
2
3
4
5
6
7
8
9
10
#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,编写一下内容

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#共享库文件名,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是动态库的名字,保存后执行命令

COPY
1
sudo make -f makefile-linux

如果没有异常则执行命令

COPY
1
sudo make -f makefile-linux install

完成后会在/usr/local/lib文件夹中生成libjspeex.so文件,如果编译时出现

COPY
1
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,然后刷新

COPY
1
2
3
vi /etc/ld.so.conf
增加一行 include /usr/local/lib
sudo ldconfig

结尾

本文只是记录了我在使用过程中遇到的一些问题,有些没有遇到的欢迎补充,另外如果知道Java如果加载jar中so文件也麻烦告知一下,现在就在头疼这个问题

Authorship: 作者
Article Link: https://raye.wang/2017/10/21/%E5%BE%AE%E4%BF%A1Speex%E8%BD%ACwav%EF%BC%8CSpeex-to-wav/
Copyright: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite Raye Blog !