【原創】【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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM