(原創)speex與wav格式音頻文件的互相轉換


我們的司信項目又有了新的需求,就是要做會議室。然而需求卻很糾結,要繼續按照原來發語音消息那樣的形式來實現這個會議的功能,還要實現語音播放的計時,暫停,語音的拼接,還要繪制頻譜圖等等。

如果是wav,mp3不論你怎么拼接,繪制頻譜圖,我也沒有問題,網上都有現成的例子。然而這一次居然讓用speex的音頻做這一切。

於是看了司信之前的發語音消息部分speex的代碼,天啊,人家錄的時候這是實時錄音實時編碼的好不好,人家放的時候也是實時解碼實時播放的好不好。你這讓我怎么通過 一個speex文件就得到全部的頻譜圖和時間啊,你讓我怎么在播放的時候暫停,然后再按一下繼續播放啊,這哪里是坑啊,這簡直就是坑爹啊。

speex格式的文件是不能暫停的,也不能直接得到時間長度和頻譜,因此只能轉化成wav或者mp3格式的才可以。要想實現上面的功能就必須實現speex文件與正常音頻格式的轉換。

這里可能有些人對安卓的錄音過程不太懂,先介紹一下(研究了這么久,就讓我賣弄一下吧)

安卓錄音的時候是使用AudioRecord來進行錄制的(當然mediarecord也可以,mediarecord強大一些),錄制后的數據稱為pcm,這就是raw(原始)數據,這些數據是沒有任何文件頭的,存成文件后用播放器是播放不出來的,需要加入一個44字節的頭,就可以轉變為wav格式,這樣就可以用播放器進行播放了。

怎么加頭,代碼在下邊:

 1 // 這里得到可播放的音頻文件  
 2     private void copyWaveFile(String inFilename, String outFilename) {
 3         FileInputStream in = null;
 4         FileOutputStream out = null;
 5         long totalAudioLen = 0;
 6         long totalDataLen = totalAudioLen + 36;
 7         long longSampleRate = AudioFileFunc.AUDIO_SAMPLE_RATE;
 8         int channels = 2;
 9         long byteRate = 16 * AudioFileFunc.AUDIO_SAMPLE_RATE * channels / 8;
10         byte[] data = new byte[bufferSizeInBytes];
11         try {
12             in = new FileInputStream(inFilename);
13             out = new FileOutputStream(outFilename);
14             totalAudioLen = in.getChannel().size();
15             totalDataLen = totalAudioLen + 36;
16             WriteWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
17             while (in.read(data) != -1) {
18                 out.write(data);
19             }
20             in.close();
21             out.close();
22         } catch (FileNotFoundException e) {
23             e.printStackTrace();
24         } catch (IOException e) {
25             e.printStackTrace();
26         }
27     }
28 
29     /** 
30      * 這里提供一個頭信息。插入這些信息就可以得到可以播放的文件。 
31      * 為我為啥插入這44個字節,這個還真沒深入研究,不過你隨便打開一個wav 
32      * 音頻的文件,可以發現前面的頭文件可以說基本一樣哦。每種格式的文件都有 
33      * 自己特有的頭文件。 
34      */
35     private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen, long totalDataLen, long longSampleRate, int channels, long byteRate) throws IOException {
36         byte[] header = new byte[44];
37         header[0] = 'R'; // RIFF/WAVE header  
38         header[1] = 'I';
39         header[2] = 'F';
40         header[3] = 'F';
41         header[4] = (byte) (totalDataLen & 0xff);
42         header[5] = (byte) ((totalDataLen >> 8) & 0xff);
43         header[6] = (byte) ((totalDataLen >> 16) & 0xff);
44         header[7] = (byte) ((totalDataLen >> 24) & 0xff);
45         header[8] = 'W';
46         header[9] = 'A';
47         header[10] = 'V';
48         header[11] = 'E';
49         header[12] = 'f'; // 'fmt ' chunk  
50         header[13] = 'm';
51         header[14] = 't';
52         header[15] = ' ';
53         header[16] = 16; // 4 bytes: size of 'fmt ' chunk  
54         header[17] = 0;
55         header[18] = 0;
56         header[19] = 0;
57         header[20] = 1; // format = 1  
58         header[21] = 0;
59         header[22] = (byte) channels;
60         header[23] = 0;
61         header[24] = (byte) (longSampleRate & 0xff);
62         header[25] = (byte) ((longSampleRate >> 8) & 0xff);
63         header[26] = (byte) ((longSampleRate >> 16) & 0xff);
64         header[27] = (byte) ((longSampleRate >> 24) & 0xff);
65         header[28] = (byte) (byteRate & 0xff);
66         header[29] = (byte) ((byteRate >> 8) & 0xff);
67         header[30] = (byte) ((byteRate >> 16) & 0xff);
68         header[31] = (byte) ((byteRate >> 24) & 0xff);
69         header[32] = (byte) (2 * 16 / 8); // block align  
70         header[33] = 0;
71         header[34] = 16; // bits per sample  
72         header[35] = 0;
73         header[36] = 'd';
74         header[37] = 'a';
75         header[38] = 't';
76         header[39] = 'a';
77         header[40] = (byte) (totalAudioLen & 0xff);
78         header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
79         header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
80         header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
81         out.write(header, 0, 44);
82     }

得到了wav文件,那我們如何轉化成speex文件呢?由於之前的項目采用的是googlecode上gauss的代碼,沒有經過太多改動,也沒有仔細研究過。這里我先請教了公司的技術達人,天虹總監(之前國內首先研究ios上使用speex庫的大牛),他說就把wav去掉header,然后把pcm數據放入的speex的encode方法里編碼就可以了,得到的數據就是speex的文件。

聽大牛一說如此簡單,還等啥,照辦,代碼寫好了,一運行就崩潰,擦,為什么呢,再運行還崩潰,錯誤提示是:

 1 JNI WARNING: JNI function SetByteArrayRegion called with exception pending

2 in Lcom/sixin/speex/Speex;.encode:([SI[BI)I (SetByteArrayRegion) 

數組越界,天啊為什么?!

於是我仔細去找了speex的源碼:

 1 extern "C"
 2 JNIEXPORT jint JNICALL Java_com_sixin_speex_Speex_encode
 3     (JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) {
 4 
 5         jshort buffer[enc_frame_size];
 6         jbyte output_buffer[enc_frame_size];
 7     int nsamples = (size-1)/enc_frame_size + 1;
 8     int i, tot_bytes = 0;
 9 
10     if (!codec_open)
11         return 0;
12 
13     speex_bits_reset(&ebits);
14 
15     for (i = 0; i < nsamples; i++) {
16         env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer);
17         speex_encode_int(enc_state, buffer, &ebits);
18     }
19     //env->GetShortArrayRegion(lin, offset, enc_frame_size, buffer);
20     //speex_encode_int(enc_state, buffer, &ebits);
21 
22     tot_bytes = speex_bits_write(&ebits, (char *)output_buffer,
23                      enc_frame_size);
24     env->SetByteArrayRegion(encoded, 0, tot_bytes,
25                 output_buffer);
26 
27         return (jint)tot_bytes;
28 }

發現了enc_frame_size 有一個恆定的值:160

然后仔細研究發現這個encode方法每次也就只能編碼160個short類型的音頻原數據,擦,大牛給我留了一個坑啊。

沒事,這也好辦,既然你只接受160的short,那我就一點一點的讀,一點一點的編碼不行么。

方法在下:

 1 public void raw2spx(String inFileName, String outFileName) {
 2 
 3         FileInputStream rawFileInputStream = null;
 4         FileOutputStream fileOutputStream = null;
 5         try {
 6             rawFileInputStream = new FileInputStream(inFileName);
 7             fileOutputStream = new FileOutputStream(outFileName);
 8             byte[] rawbyte = new byte[320];
 9             byte[] encoded = new byte[160];
10             //將原數據轉換成spx壓縮的文件,speex只能編碼160字節的數據,需要使用一個循環
11             int readedtotal = 0;
12             int size = 0;
13             int encodedtotal = 0;
14             while ((size = rawFileInputStream.read(rawbyte, 0, 320)) != -1) {
15                 readedtotal = readedtotal + size;
16                 short[] rawdata = byteArray2ShortArray(rawbyte);
17                 int encodesize = speex.encode(rawdata, 0, encoded, rawdata.length);
18                 fileOutputStream.write(encoded, 0, encodesize);
19                 encodedtotal = encodedtotal + encodesize;
20                 Log.e("test", "readedtotal " + readedtotal + "\n size" + size + "\n encodesize" + encodesize + "\n encodedtotal" + encodedtotal);
21             }
22             fileOutputStream.close();
23             rawFileInputStream.close();
24         } catch (Exception e) {
25             Log.e("test", e.toString());
26         }
27 
28     }

注意speex.encode方法的第一個參數是short類型的,這里需要160大小的short數組,所以我們要從文件里每次讀取出320個byte(一個short等於兩個byte這不用再解釋了吧)。轉化成short數組之后在編碼。

經過轉化發現speex的編碼能力好強大,1.30M的文件,直接編碼到了80k,好膩害呦。

這樣在傳輸的過程中可以大大的減少流量,只能說speex技術真的很牛x。聽說后來又升級了opus,不知道會不會更膩害呢。

 

編碼過程實現了,接下來就是如何解碼了,后來測試又發現speex的編碼也是每次只能解碼出來160個short,要不怎么說坑呢。

那個方法是這樣子的

 1 decsize = speex.decode(inbyte, decoded, readsize); 

既然每次都必須解碼出160個short來,那我放進去的inbyte是多少個byte呢,你妹的也不告訴我啊???

不告訴我,我也有辦法,之前不是每次編碼160個short嗎?看看你編完之后是多少個byte不就行了?

經過測試,得到160個short編完了是20個byte,也就是320個byte壓縮成了20個byte,數據縮小到了原來的1/16啊,果然牛x。

既然知道了是20,那么每次從壓縮后的speex文件里讀出20個byte來解碼,這樣就應該可以還原數據了。

 1 public void spx2raw(String inFileName, String outFileName) {
 2         FileInputStream inAccessFile = null;
 3         FileOutputStream fileOutputStream = null;
 4         try {
 5             inAccessFile = new FileInputStream(inFileName);
 6             fileOutputStream = new FileOutputStream(outFileName);
 7             byte[] inbyte = new byte[20];
 8             short[] decoded = new short[160];
 9             int readsize = 0;
10             int readedtotal = 0;
11             int decsize = 0;
12             int decodetotal = 0;
13             while ((readsize = inAccessFile.read(inbyte, 0, 20)) != -1) {
14                 readedtotal = readedtotal + readsize;
15                 decsize = speex.decode(inbyte, decoded, readsize);
16                 fileOutputStream.write(shortArray2ByteArray(decoded), 0, decsize*2);
17                 decodetotal = decodetotal + decsize;
18                 Log.e("test", "readsize " + readsize + "\n readedtotal" + readedtotal + "\n decsize" + decsize + "\n decodetotal" + decodetotal);
19             }
20             fileOutputStream.close();
21             inAccessFile.close();
22         } catch (Exception e) {
23             Log.e("test", e.toString());
24         }
25     }

當然解碼出來的文件是pcm的原數據,要想播放必須加44個字節的wav的文件頭,上面已經說過了,有興趣的可以自己試試。

ps:wav文件去頭轉成spx然后再轉回wav播放出來的文件,雖然時長沒有變,但是聲音變小了,貌似還有了點點的噪音。因此我懷疑speex壓縮式有損壓縮,不過如果只是語音的話,還是可以聽清楚的,里面的具體算法我不清楚,如果大家有時間可以自己研究研究。

 

昨天晚上又經過了一輪測試,發現直接壓縮wav的原數據到speex這個壓縮效率只是壓縮為原來數據大小的1/16,而我用gauss的算法錄出來的spx文件壓縮效率要高很多,比如用原始音頻錄了7s,wav數據是1.21M,而gauss算法得到的speex文件只有8k,采用我的方法直接壓縮后的speex文件為77k。而用安卓的mediarecord錄音得到的amr格式的文件只有13k,如果使用我提供的方法錄音那還不如使用安卓自帶的api錄制amr格式的音頻呢,還費這么大勁搞這玩意兒干啥?大牛還是有些東西沒有告訴我們,這還需要我們自己去研究。

差距為什么這么大呢?我又去看了gauss的方法,他生成speex文件的流程經過了ogg編碼,過程如下:

1.首先它錄音的過程與我們錄音的過程都是一樣的,都是先錄制pcm的原數據

2.錄制完成后他也是用了speex先壓縮

3.speex壓縮后的數據存儲的時候,他封裝了speexwriter的一個類,speexwriter又調用了speexwriterClient的一個類

,而在speexwriterClient里又發現了oggspeexwriter的類。也就是說,他在把speex壓縮后的20個byte放入到文件的時候又進行了一次ogg編碼

這樣我們就找到原因了,但是對於ogg的編碼我不熟悉,還有待研究。如果有啥成果了,就請期待我下一篇博客吧。

 

更正:之所以我錄制出來的wav音頻大,以及編碼成的speex文件比gauss的文件大的原因不只有ogg編碼的問題,還有另外一個更重要的原因:設置的采樣率不同,gauss的demo里設置的采樣率額為8000,而我設置的是標准的44100的采樣率額,因此采集到的數據本來就大很多

然后我又將采樣率改成了8000,然后7s的原始錄音大小由1M多減小到200k多一點了,然后直接轉成speex后為13k大小,跟amr可以說不相上下。請原諒我的錯誤。(T_T)

 

代碼鏈接如下:

https://github.com/dongweiq/study/tree/master/Record

我的github地址:https://github.com/dongweiq/study

歡迎關注,歡迎star o(∩_∩)o 。有什么問題請郵箱聯系 dongweiqmail@gmail.com qq714094450

 


免責聲明!

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



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