【原创】【JNI】OPUS压缩与解压的JNI调用(.DLL版本)


 


OPUS压缩与解压的JNI调用(.DLL版本)


一、写在开头:

      理论上讲,这是我在博客园的第一篇原创的博客,之前也一直想找个地方写点东西,把最近做的一些东西归纳总结下,但是一般工程做完了一高兴就把东西丢一边就很久不碰了,久而久之就淡忘了。这不是一个很好的习惯,古人也说更好记性不如烂笔头,无论是做学术还是做工程,定期总结与归纳都是一个不错的巩固与提高的方法。另外,也希望给后来者一点可行性的参考(有误导的地方请轻喷)。

 


二、引言:

      言归正传,最近在做Android的一个工程,大体是实现在Android平台的录音、压缩、传输、解压、识别的功能。其中,识别包括语音识别与音乐片段的识别,这是两个很大的模块,有其他的工程师负责。我负责录音、压缩、传输、解压等几个小模块。

       其中录音功能是比较简单的,网上参考的范例比较多,以后找时间再整理下。压缩部分包括语音压缩(VAD+压缩)与旋律的压缩,因为用过Speex压缩,所以语音压缩模块就直接沿用了之前的范例,但是Speex只能对语音进行压缩,不能对旋律进行压缩(失真较大,影响片段的检索),所以考虑采用一种较新的压缩方式:Opus压缩。

 


三、OPUS简介:

       Opus编码器 是一个有损声音编码的格式,由互联网工程任务组(IETF)进来开发,适用于网络上的实时声音传输,标准格式为RFC 6716。Opus 格式是一个开放格式,使用上没有任何专利或限制。

      Opus压缩几乎包含了Speex压缩的所有功能,并且支持对旋律的压缩,可以说功能是相当强大的,同Speex一样,Opus也是开源的,可以在官网上下载到Opus的源码。我下载的是最新的1.1版本的,解压后如下图所示:

      

      但是除了官网外,Opus压缩的参考比较少,跨平台应用的就更少了点,做惯了伸手党的我对此相当郁闷,所以就花了点时间研究了下,实现了Android客户端(.so库)与windows服务器端(.dll库)的调用。

     服务器端相对简单一点,基本思路是:利用VS2010创建生成相应的dll库,然后在服务器端利用NDK实现JNI接口,继而在主函数中实现调用。

 


四、Opus的DLL生成过程

      Opus使用C语言实现的,并且官网上给了几个不错的DEMO,要实现Java对C/C++跨平台的调用,首先我们要保证我们的C语言接口是正确的,也就是说,首先要在VS2010等平台上将调用的函数跑通,只有测试函数通过了,才能保证我们移植到JVM上的程序是正确的。之后要根据正确的测试函数书写C语言接口,然后将C语言写的接口编译生成相应的dll库。

4.1、书写完整的测试函数

      打开Opus官网上给的工程,其中,测试了一下,test_opus_encode工程和test_opus_decode工程里面都有完整的压缩与解压实体。所以,我们只需要改写其中的一个工程的主函数OK了,主函数书写的基本思路为:压缩与解压对象的创建、参数的设置、压缩/解压、资源的释放,代码如下:

  1 #ifdef HAVE_CONFIG_H
  2 #include "config.h"
  3 #endif
  4 #include <stdio.h>
  5 #include <stdlib.h>
  6 #include <limits.h>
  7 #include <stdint.h>
  8 #include <math.h>
  9 #include <string.h>
 10 #include <time.h>
 11 #if (!defined WIN32 && !defined _WIN32) || defined(__MINGW32__)
 12 #include <unistd.h>
 13 #else
 14 #include <process.h>
 15 #define getpid _getpid
 16 #endif
 17 #include "opus_multistream.h"
 18 #include "opus.h"
 19 #include "../src/opus_private.h"
 20 //#include "test_opus_common.h"
 21 
 22 /*#define MAX_PACKET (1500)
 23 #define SAMPLES (48000*30)
 24 #define SSAMPLES (SAMPLES/3)
 25 #define MAX_FRAME_SAMP (5760)*/
 26 
 27 int error;
 28 int Fs=16000;
 29 int channels=1;
 30 int application=OPUS_APPLICATION_AUDIO;
 31 OpusEncoder *enc=NULL;
 32 
 33 opus_int32 bitrate_bps=16000;
 34 int bandwidth = OPUS_AUTO;
 35 int use_vbr = 0;
 36 int cvbr=1;
 37 int complexity = 8;
 38 
 39 int frame_size_1=320;
 40 int frame_size_2=320;
 41 opus_int32 max_data_bytes=2500;
 42 
 43 unsigned char *data;
 44 const opus_int16 *pcm;
 45 opus_int16 *pcm_re;
 46  OpusDecoder *dec=NULL;
 47  unsigned char data_1[60][40];
 48  opus_int16 pcm_re_1[60][320];
 49  opus_int16 pcm_bf[60][320];
 50 
 51 #define PI (3.141592653589793238462643f)
 52     int i;
 53     int j;
 54     int n;
 55     int m;
 56     int packet_loss_perc=0;
 57 
 58     int max_frame_size = 2*16000;
 59     int output_samples;
 60     opus_uint32 dec_final_range;
 61     int dur;
 62 
 63     FILE *fp;
 64     FILE *fp_1;
 65     int num_pcm;
 66     int num_pcm_re;
 67     short temp_end[19200];
 68 
 69 int main(int _argc, char **_argv)
 70 {
 71     short temp[19200];
 72     FILE *foo;
 73     printf("I am lzhen\n");
 74     //memset(temp,1,600);
 75     
 76  
 77     if((fp=fopen("e:\\dkdt.pcm","rb"))==NULL)
 78     {
 79        printf("cant open the file");
 80     }
 81     
 82 num_pcm=fread(&temp[0],sizeof(short),19200,fp);  
 83 
 84 printf("%d ",num_pcm);
 85 //for(i=0;i<19200;i++)
 86 //{
 87     //printf("%d ",temp[i]);
 88 //}
 89    fclose(fp);//将打开的文件关闭
 90 
 91    for (i=0;i<60;i++)
 92        for(j=0;j<320;j++)
 93        {
 94        pcm_bf[i][j]=temp[320*i+j];
 95        }
 96 
 97 
 98     enc = opus_encoder_create(Fs, channels, application, &error);
 99 
100     if (error != OPUS_OK)
101     {printf("创建失败");
102     }else
103     {printf("创建成功");
104     }
105 
106     //for(pcm=number,i=0;i<2000;i++,pcm++)
107     //{printf("%d  ",*pcm);}
108 
109        opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate_bps));
110        opus_encoder_ctl(enc, OPUS_SET_BANDWIDTH(bandwidth));
111        opus_encoder_ctl(enc, OPUS_SET_VBR(use_vbr));
112        opus_encoder_ctl(enc, OPUS_SET_VBR_CONSTRAINT(cvbr));
113        opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
114        opus_encoder_ctl(enc, OPUS_SET_PACKET_LOSS_PERC(packet_loss_perc));
115        /*opus_encoder_ctl(enc, OPUS_SET_INBAND_FEC(use_inbandfec));
116        opus_encoder_ctl(enc, OPUS_SET_FORCE_CHANNELS(forcechannels));
117        opus_encoder_ctl(enc, OPUS_SET_DTX(use_dtx));
118        opus_encoder_ctl(enc, OPUS_SET_PACKET_LOSS_PERC(packet_loss_perc));
119 
120        opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&skip));
121        opus_encoder_ctl(enc, OPUS_SET_LSB_DEPTH(16));
122        opus_encoder_ctl(enc, OPUS_SET_EXPERT_FRAME_DURATION(variable_duration));*/
123 
124        for(i=0;i<60;i++)
125        {
126        pcm=pcm_bf[i]; 
127        data=data_1[i];
128 
129     n=opus_encode(
130          enc,
131          pcm,
132          frame_size_1,
133          data,
134          max_data_bytes);
135        }//OPUS压缩
136     opus_encoder_destroy(enc);//释放资源
137     dec = opus_decoder_create(Fs, channels, &error);
138     
139     output_samples = max_frame_size;
140     /*opus_decoder_ctl(dec, OPUS_GET_LAST_PACKET_DURATION(&output_samples));
141     opus_decoder_ctl(dec, OPUS_GET_FINAL_RANGE(&dec_final_range));
142     opus_decoder_ctl(dec, OPUS_GET_LAST_PACKET_DURATION(&dur));*/
143 
144     for(i=0;i<60;i++)
145     {
146     data=data_1[i];
147     pcm_re=pcm_re_1[i];
148     m=opus_decode(
149          dec,
150          data,
151          n,
152          pcm_re,
153          frame_size_2,
154          0);
155     }
156     opus_decoder_destroy(dec);
157     printf("\n\n\n解压后的长度为:%d\n  ",m);
158 
159    for (i=0;i<60;i++)
160        for(j=0;j<320;j++)
161        {
162        temp_end[320*i+j]=pcm_re_1[i][j];
163        }
164 
165     if((fp_1=fopen("e:\\dkdt_re.pcm","wb"))==NULL)
166     {
167        printf("cant open the file");
168     }
169     num_pcm_re=fwrite(&temp_end[0],sizeof(short),19200,fp_1);  
170 getchar();
171 }

      这里面我用一个dkdt.pcm的语音文件做范例,分帧进行压缩与解压,每一帧为320short,压缩后为40byte,压缩比为16,之后进行解压还原,生成相应的dkdt_out.pcm文件。这里给出我的测试结果,如下图所示:第一个是压缩前的音频信号,第二个是压缩解压后恢复的音频文件。

 

 

     可以看到压缩前后还原后的波形几乎是一致的(听起来无差别),所以我们写的测试函数中压缩与解压是没有问题的。这一步很关键,是我们接下来的基础。

4.2、JNI接口的C语言部分

     上一步OK了之后,就可以根据书写的测试函数书写JNI接口的C语言部分,这部分是比较简单的,稍微改下就OK了,JNI的书写规则,网上参考的东西比较详细,这里就不在赘述,直接给出代码如下:

  1 #ifdef HAVE_CONFIG_H
  2 #include "config.h"
  3 #endif
  4 #include "StdAfx.h"
  5 #include <stdio.h>
  6 #include <stdlib.h>
  7 #include <limits.h>
  8 #include <stdint.h>
  9 #include <math.h>
 10 #include <string.h>
 11 #include "jni.h"
 12 #include <time.h>
 13 #if (!defined WIN32 && !defined _WIN32) || defined(__MINGW32__)
 14 #include <unistd.h>
 15 #else
 16 #include <process.h>
 17 #define getpid _getpid
 18 #endif
 19 #include "opus_multistream.h"
 20 #include "opus.h"
 21 #include "../src/opus_private.h"
 22 
 23 int opus_num;
 24 int pcm_num;
 25 short pcm_data_encoder[320];
 26 unsigned char opus_data_encoder[40];
 27 short pcm_data_decoder[320];
 28 unsigned char opus_data_decoder[40];
 29 OpusEncoder *enc=NULL;
 30 OpusDecoder *dec=NULL;
 31 
 32 
 33 
 34 JNIEXPORT int JNICALL Java_audioTest_OpusJni_test(JNIEnv *env, jobject thiz)
 35 {
 36   return 103;//测试函数
 37 }
 38 
 39 JNIEXPORT void JNICALL Java_audioTest_OpusJni_Init(JNIEnv *env, jobject thiz)
 40 {
 41   int error;
 42   int Fs=16000;
 43   int channels=1;
 44   int application=OPUS_APPLICATION_AUDIO;
 45   
 46   opus_int32 bitrate_bps=16000;
 47   int bandwidth = OPUS_AUTO;
 48   int use_vbr = 0;
 49   int cvbr=1;
 50   int complexity = 8;
 51   int packet_loss_perc=0;
 52   
 53   enc = opus_encoder_create(Fs, channels, application, &error);
 54   dec = opus_decoder_create(Fs, channels, &error);
 55 
 56    opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate_bps));
 57    opus_encoder_ctl(enc, OPUS_SET_BANDWIDTH(bandwidth));
 58    opus_encoder_ctl(enc, OPUS_SET_VBR(use_vbr));
 59    opus_encoder_ctl(enc, OPUS_SET_VBR_CONSTRAINT(cvbr));
 60    opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
 61    opus_encoder_ctl(enc, OPUS_SET_PACKET_LOSS_PERC(packet_loss_perc));
 62 }
 63 
 64 JNIEXPORT jint JNICALL Java_audioTest_OpusJni_opusEncoder(JNIEnv *env, jobject thiz,jshortArray encoder_insrc,jbyteArray encoder_out)
 65 {
 66   int frame_size=320;
 67   opus_int32 max_data_bytes=2500;
 68   jshort* pcm_data_encoder=(*env)->GetShortArrayElements(env, encoder_insrc, 0);
 69   jbyte* opus_data_encoder=(*env)->GetByteArrayElements(env, encoder_out, 0);
 70 
 71   opus_num=opus_encode(
 72          enc,
 73          pcm_data_encoder,
 74          frame_size,
 75          opus_data_encoder,
 76          max_data_bytes);
 77 
 78   (*env)->ReleaseShortArrayElements(env, encoder_insrc, pcm_data_encoder, 0);
 79   (*env)->ReleaseByteArrayElements(env, encoder_out, opus_data_encoder, 0);
 80   if((*env)->ExceptionCheck(env))
 81      {
 82         return - 1;
 83      }
 84   return opus_num;
 85 }
 86 
 87 JNIEXPORT jint JNICALL Java_audioTest_OpusJni_opusDecoder(JNIEnv *env, jobject thiz,jbyteArray decoder_insrc,jshortArray decoder_out)
 88 {
 89   int frame_size=320;
 90   jshort* pcm_data_decoder=(*env)->GetShortArrayElements(env, decoder_out, 0);
 91   jbyte* opus_data_decoder=(*env)->GetByteArrayElements(env, decoder_insrc, 0);
 92   pcm_num=opus_decode(
 93          dec,
 94          opus_data_decoder,
 95          40,
 96          pcm_data_decoder,
 97          frame_size,
 98          0);
 99          
100   (*env)->ReleaseShortArrayElements(env, decoder_out, pcm_data_decoder, 0);
101   (*env)->ReleaseByteArrayElements(env, decoder_insrc, opus_data_decoder, 0);
102   if((*env)->ExceptionCheck(env))
103      {
104         return - 1;
105      }
106   return pcm_num;
107 }
108 
109 JNIEXPORT void JNICALL Java_audioTest_OpusJni_Destroy(JNIEnv *env, jobject thiz)
110 {
111   opus_encoder_destroy(enc);
112   opus_decoder_destroy(dec);
113 }

     这里面稍微说一下,一个师兄跟我说,写这种跨平台的东西总会遇到各种莫名奇妙的问题,但是一切的BUG都是有原因的,这里写一个JNIEXPORT int JNICALL Java_audioTest_OpusJni_test(JNIEnv *env, jobject thiz)这样的测试函数,就可以简单地判断我们的JNI时候调用成功,成功的话,其他的就是调用函数内部的问题了。这样可以很快的缩小BUG的范围。

4.3、编译生成相应的dll库

      写完JNI接口之后,就要将我们用到的所有依赖的文件(包括.c 、.h)一起打包编译为.dll文件。在VS2010中新建dll工程,dll工程里面有一个DEMO,我们这里用不到,可以讲里面的东西删了,将上一步写的接口文件拷贝其中,如下图所示:

       

      和一般的C/C++工程一样,我们需要添加依赖的.C文件与.h文件,首先我们需要添加头文件,Opus依赖头文件分布的比较零散,不像speex全部在include文件里面,不过这里我们也只需要指定好路径,编译器会自动链接到相应的头文件,如下图所示:

     

      此外,我们还要设置好依赖(调用)的.C文件,最直观的办法是将所有需要的.C文件都添加进工程,要不重不漏,重了会报重复定义的错误,漏了会报缺少定义的错误,编译Android的依赖库.so文件的时候,我的确是这么做的,这个过程需要一定时间的调试,要理清开源库中函数之间的关系,知道需要调用哪些函数。这里其实有个更加简单粗暴地办法,之前我们第一步编译测试工程的时候,会生成四个相应的.lib库,如下图所示:

   

      只需要指定依赖的库文件的路径,如下图所示:

     

      并将这四个库添加到依赖库中,如下图,编译器在编译的时候就可以直接连接到相应的函数实体。

     

      这步成功之后,就可以在Release或者Debug文件夹中找到我们编译成功的opus.dll文件,157KB,沉甸甸的。接下来就就是Java部分对dll文件的调用了。

 


五、Opus的DLL调用过程

      首先,我们需要书写Java端的JNI接口部分,这部分代码比较简单,直接给出。如下所示:

 1 package audioTest;
 2 /**
 3  * 压缩库
 4  * 
 5  * @author lzhen
 6  *
 7  */
 8 public class OpusJni
 9 {
10     static
11     {
12         System.loadLibrary("opus");
13     }
14     public native int test();
15     public native void Init(); // 压缩初始化
16     public native int opusEncoder(short[] src, byte[] out); // 压缩数据,长度为320short->40byte
17     public native int opusDecoder(byte[] src, short[] out);// 解压缩数据,长度为40byte->320short
18     public native void Destroy(); // 释放内存
19 }

      剩下的就是简单的Java对象的调用了。这里,我们可以首先测试下test()函数是否成功,如果成功的话,说明dll库至少是没问题的。

     在之后.so库的调用的时候再具体总结一下Opus压缩与解压调用的详细过程,这里就不再赘述了。


六、BUG与调试

BUG1:

Exception in thread "main" java.lang.UnsatisfiedLinkError: E:\jni\testopus\libs\opus.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform

    at java.lang.ClassLoader$NativeLibrary.load(Native Method)

    at java.lang.ClassLoader.loadLibrary1(ClassLoader.java:1965)

    at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1890)

    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1880)

    at java.lang.Runtime.loadLibrary0(Runtime.java:849)

    at java.lang.System.loadLibrary(System.java:1088)

    at audioTest.OpusJni.<clinit>(OpusJni.java:12)

    at audioTest.Server.main(Server.java:22)

 

      DEBUG1:

     这里是因为Opus官网上给的程序是根据32bit系统给出的,如果采用64bit系统,在VS2010中设置为X64会报错,所以只能按照X86编译,但是如果JDK是64bit的话,在eclipse中就会报上面的错误。

     解决的办法是:将64bit的JDK卸载掉,安装一个32bit的JDK就可以编译通过,但是有一个问题,JDK如果卸载重装的话,有可能会失败,原因是重装的时候,之前注册表没有完全删除的问题,可以用Windows Installer Clean Up将之前的注册表彻底删除。不过,我觉得最简单的办法是重新安装一个其他版本的JDK就OK了,方法是死的,人是活的,我喜欢简单粗暴又能解决问题的方法。

 

 

BUG2:

java.lang.UnsatisfiedLinkError: E:\jni\testopus\libs\opus.dll: Can't find dependent libraries。

 

      DEBUG2:

      意思是找不到依赖库,我们编译生成的opus.dll文件可以放在系统的DLL库中(C:\Windows\System32),当然,也可以直接放在工程中,这样都是可以load到的,所以Can't find dependent libraries不是说找不到我们编译生成的DLL文件,而是我们编译生成的DLL文件也需要一些依赖库,这里面有两种情况:

      一种是,我们在一台主机上编译生成的,拿到另外一台主机上面跑,可能两台主机配置或者是其他种种不同,系统DLL库会有所差异,可以用DLL依赖查看工具这个软件查找缺失的依赖库,如下图所示:

      

 

      黄色标记表示缺失的DLL,我这里是因为,换了一台没有安装VS2010的主机上测试导致的,解决办法很简单,可以在网上下载缺失的DLL放到C:\Windows\System32中,或者,直接在编译的那台PC上拷贝,都是可以的。

     第二种情况比较复杂一点,就是,我们在编译的时候,没有添加最原始的.lib库,这在不同的工程中会有所不同,一般用DLL依赖查看工具检察缺失的为.exe文件就是这种情况,那就要回头检察,我们在编译之前添加的.lib文件是不是有问题了。

 

BUG3: 

#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x15c21781, pid=5264, tid=14016
#
# JRE version: Java(TM) SE Runtime Environment (8.0_11-b12) (build 1.8.0_11-b12)
# Java VM: Java HotSpot(TM) Client VM (25.11-b03 mixed mode, sharing windows-x86 )
# Problematic frame:
# C [test_opus_decode.exe+0x51781]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\test\Desktop\Music0808_sever\hs_err_pid5264.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.sun.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

 

      DEBUG3:

      这个bug在很多游戏里面会出现,在JNI的跨平台调用中,出现类似情况,其实也是容易判断的,首先,我们要看一下,我们在Java端查看test()函数是否运行成功,如果成功过了,那么说明DLL库的编译是没有问题的,那么接下来,就可以通过单步调试,查找具体在哪一个调用函数出现问题了,同时,在C语言端要通过注释等方式联合调试,最终确定具体是哪一个函数的哪一句出现了问题,目前,我还没有发现在DLL内部单步调试的方法,所以只能用这种方法尝试,不过效果还是很明显的,我的问题是初始化函数出现了一点问题。

     另外,现在eclipse已经支持直接的NDK编译了,所以在Android平台上,好像是可以对.so库进行单步调试的,这个以后再说。

 


七、后记

      写到这里,本章差不多结束了,有机会希望能够跟大家一起沟通交流,相互促进,相互提高。最后,跟大家分享一句OPUS官网上对开源的解释,我觉得很赞!

       ——Imagine a road system where each type of car could only drive on its own manufacturer's pavement. We all benefit from living in a world where all the roads are connected. This is why Opus, unlike many codecs, is free.

 

Reference:

        https://wiki.xiph.org/Main_Page

        http://int.zhubajie.com/case-studies/3307420?utm_source=TiaoZhuan_RenWuYe&utm_medium=RenWuYe_3307420&utm_campaign=TiaoZhuan

 

posted @ 2014-08-11 23:44  Charles04  阅读(3528)  评论(6编辑  收藏  举报