咱們的司信項目又有了新的需求,就是要作會議室。然而需求卻很糾結,要繼續按照原來發語音消息那樣的形式來實現這個會議的功能,還要實現語音播放的計時,暫停,語音的拼接,還要繪製頻譜圖等等。ios
若是是wav,mp3不論你怎麼拼接,繪製頻譜圖,我也沒有問題,網上都有現成的例子。然而這一次竟然讓用speex的音頻作這一切。git
因而看了司信以前的發語音消息部分speex的代碼,天啊,人家錄的時候這是實時錄音實時編碼的好很差,人家放的時候也是實時解碼實時播放的好很差。你這讓我怎麼經過 一個speex文件就獲得所有的頻譜圖和時間啊,你讓我怎麼在播放的時候暫停,而後再按一下繼續播放啊,這哪裏是坑啊,這簡直就是坑爹啊。github
speex格式的文件是不能暫停的,也不能直接獲得時間長度和頻譜,所以只能轉化成wav或者mp3格式的才能夠。要想實現上面的功能就必須實現speex文件與正常音頻格式的轉換。算法
這裏可能有些人對安卓的錄音過程不太懂,先介紹一下(研究了這麼久,就讓我賣弄一下吧)api
安卓錄音的時候是使用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的文件。google
聽大牛一說如此簡單,還等啥,照辦,代碼寫好了,一運行就崩潰,擦,爲何呢,再運行還崩潰,錯誤提示是:編碼
1 JNI WARNING: JNI function SetByteArrayRegion called with exception pending spa
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