Android中實現(xiàn)多段wav音頻文件拼接

博客搬遷到這里 http://blog.fdawei.club柠贤,歡迎訪問,大家一起學(xué)習(xí)交流楷扬。

WAV為微軟公司開發(fā)的一種聲音文件格式翘鸭,它符合RIFF(Resource Interchange File Format)文件規(guī)范,用于保存Windows平臺的音頻信息資源鸳址,被Windows平臺及其應(yīng)用程序所廣泛支持瘩蚪。
由于項目中需要接入訊飛的語音聽寫進行快速錄入,并且同時保存語音文件稿黍。訊飛語音聽寫的SDK只支持保存語音文件為pcm或者wav這兩種格式疹瘦。訊飛的語音聽寫服務(wù)有很多限制,比如前后端點允許靜音最長10秒巡球、一次聽寫連續(xù)不能超過60秒言沐。項目中需要支持長時間不間斷語音聽寫,和產(chǎn)品懟了很久酣栈,經(jīng)過不懈的抗?fàn)幭找龋詈筮€是我妥協(xié)了。訊飛語音聽寫的SDK提供了一些回調(diào)矿筝,在超時中斷時起便,會回調(diào)onEndOfSpeech方法,這樣我們就可以在這里馬上重新開始啟動聽寫跋涣。但是這會引起另一個問題缨睡,錄制的音頻文件最后是一段一段的,最后還得把他們進行拼接陈辱。第一次使用訊飛的語音聽寫SDK奖年,不是很熟,不知道有沒有哪位大神有更好的解決辦法沛贪,求賜教啊啊啊啊陋守。震贵。。
尋找了很久水评,在Android的API中沒找到可以實現(xiàn)wav拼接的方法猩系,只能自己去實現(xiàn)了。萬幸的是wav格式的結(jié)構(gòu)還比較簡單中燥。

WAV文件格式

WAV文件格式

(本來是使用table編輯的表格寇甸,簡書上竟然不支持,沒辦法只能截了個圖放上來了)
可以看出疗涉,WAV文件主要是以四種chunk組成拿霉,這里我們分別稱呼為riff chunk、fmt chunk咱扣、fact chunk和data chunk绽淘,其中fact chunk不是必須的,大部分時候的沒有闹伪。所以我在查閱資料的額時候沪铭,發(fā)現(xiàn)很多解析WAV文件的代碼都直接認(rèn)為其只有固定的44字節(jié)的頭部。
此格式來源于百度百科偏瓤,奇怪的是維基百科中也認(rèn)為WAV具有一個44字節(jié)的固定頭部杀怠,如果哪位大神知道的,可以告訴我一下硼补。

WAV拼接實現(xiàn)方法

由于這里采集的音頻相關(guān)參數(shù)一致驮肉,做我們?nèi)テ渲幸欢蔚念^部作為拼接后的音頻的頭部。但是也不是這樣就可以了已骇。從上面WAV的格式中可以看出离钝,頭部中兩個位置的數(shù)據(jù)需要修改。1褪储、riff chunk中的size值卵渴;2、data chunk的size值鲤竹。因此可以先將其他數(shù)據(jù)的data chunk部分的數(shù)據(jù)追加到結(jié)果文件中浪读,最后寫入這兩個地方的值。
好了辛藻,是時候上代碼了碘橘。。吱肌。

實現(xiàn)代碼

public class WavMergeUtil {

  public static void mergeWav(List<File> inputs, File output) throws IOException {
    if (inputs.size() < 1) {
      return;
    }
    FileInputStream fis = new FileInputStream(inputs.get(0));
    FileOutputStream fos = new FileOutputStream(output);
    byte[] buffer = new byte[2048];
    int total = 0;
    int count;
    while ((count = fis.read(buffer)) > -1) {
      fos.write(buffer, 0, count);
      total += count;
    }
    fis.close();
    for (int i = 1; i < inputs.size(); i++) {
      File file = inputs.get(i);
      Header header = resolveHeader(file);
      FileInputStream dataInputStream = header.dataInputStream;
      while ((count = dataInputStream.read(buffer)) > -1) {
        fos.write(buffer, 0, count);
        total += count;
      }
      dataInputStream.close();
    }
    fos.flush();
    fos.close();
    Header outputHeader = resolveHeader(output);
    outputHeader.dataInputStream.close();
    RandomAccessFile res = new RandomAccessFile(output, "rw");
    res.seek(4);
    byte[] fileLen = intToByteArray(total + outputHeader.dataOffset - 8);
    res.write(fileLen, 0, 4);
    res.seek(outputHeader.dataSizeOffset);
    byte[] dataLen = intToByteArray(total);
    res.write(dataLen, 0, 4);
    res.close();
  }

  /**
   * 解析頭部痘拆,并獲得文件指針指向數(shù)據(jù)開始位置的InputStreram,記得使用后需要關(guān)閉
   */
  private static Header resolveHeader(File wavFile) throws IOException {
    FileInputStream fis = new FileInputStream(wavFile);
    byte[] byte4 = new byte[4];
    byte[] buffer = new byte[2048];
    int readCount = 0;
    Header header = new Header();
    fis.read(byte4);//RIFF
    fis.read(byte4);
    readCount += 8;
    header.fileSizeOffset = 4;
    header.fileSize = byteArrayToInt(byte4);
    fis.read(byte4);//WAVE
    fis.read(byte4);//fmt
    fis.read(byte4);
    readCount += 12;
    int fmtLen = byteArrayToInt(byte4);
    fis.read(buffer, 0, fmtLen);
    readCount += fmtLen;
    fis.read(byte4);//data or fact
    readCount += 4;
    if (isFmt(byte4, 0)) {//包含fmt段
      fis.read(byte4);
      int factLen = byteArrayToInt(byte4);
      fis.read(buffer, 0, factLen);
      fis.read(byte4);//data
      readCount += 8 + factLen;
    }
    fis.read(byte4);// data size
    int dataLen = byteArrayToInt(byte4);
    header.dataSize = dataLen;
    header.dataSizeOffset = readCount;
    readCount += 4;
    header.dataOffset = readCount;
    header.dataInputStream = fis;
    return header;
  }

  private static boolean isRiff(byte[] bytes, int start) {
    if (bytes[start + 0] == 'R' && bytes[start + 1] == 'I' && bytes[start + 2] == 'F' && bytes[start + 3] == 'F') {
      return true;
    } else {
      return false;
    }
  }

  private static boolean isFmt(byte[] bytes, int start) {
    if (bytes[start + 0] == 'f' && bytes[start + 1] == 'm' && bytes[start + 2] == 't' && bytes[start + 3] == ' ') {
      return true;
    } else {
      return false;
    }
  }

  private static boolean isData(byte[] bytes, int start) {
    if (bytes[start + 0] == 'd' && bytes[start + 1] == 'a' && bytes[start + 2] == 't' && bytes[start + 3] == 'a') {
      return true;
    } else {
      return false;
    }
  }

  /**
   * 將int轉(zhuǎn)化為byte[]
   */
  private static byte[] intToByteArray(int data) {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array();
  }

  /**
   * 將short轉(zhuǎn)化為byte[]
   */
  private static byte[] shortToByteArray(short data) {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array();
  }

  /**
   * 將byte[]轉(zhuǎn)化為short
   */
  private static short byteArrayToShort(byte[] b) {
    return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort();
  }

  /**
   * 將byte[]轉(zhuǎn)化為int
   */
  private static int byteArrayToInt(byte[] b) {
    return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt();
  }

  /**
   * 頭部部分信息
   */
  static class Header {
    public int fileSize;
    public int fileSizeOffset;
    public int dataSize;
    public int dataSizeOffset;
    public int dataOffset;
    public FileInputStream dataInputStream;
  }
}

這里int氮墨、short相互轉(zhuǎn)化的時候需要考慮大小端的問題纺蛆。
免責(zé)聲明:(暈吐葵。。桥氏。温峭。。字支。)
此文章內(nèi)容僅作為參考凤藏,能力有限,難免會有一些不足之處祥款,歡迎大家指正清笨,相互學(xué)習(xí),共同進步刃跛。。苛萎。哈哈

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桨昙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子腌歉,更是在濱河造成了極大的恐慌蛙酪,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翘盖,死亡現(xiàn)場離奇詭異桂塞,居然都是意外死亡,警方通過查閱死者的電腦和手機馍驯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門阁危,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人汰瘫,你說我怎么就攤上這事狂打。” “怎么了混弥?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵趴乡,是天一觀的道長。 經(jīng)常有香客問我蝗拿,道長晾捏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任哀托,我火速辦了婚禮惦辛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萤捆。我一直安慰自己裙品,他們只是感情好俗批,可當(dāng)我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著市怎,像睡著了一般岁忘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上区匠,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天干像,我揣著相機與錄音,去河邊找鬼驰弄。 笑死麻汰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的戚篙。 我是一名探鬼主播五鲫,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼岔擂!你這毒婦竟也來了位喂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤乱灵,失蹤者是張志新(化名)和其女友劉穎塑崖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痛倚,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡规婆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝉稳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抒蚜。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颠区,靈堂內(nèi)的尸體忽然破棺而出削锰,到底是詐尸還是另有隱情,我是刑警寧澤毕莱,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布器贩,位于F島的核電站,受9級特大地震影響朋截,放射性物質(zhì)發(fā)生泄漏蛹稍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一部服、第九天 我趴在偏房一處隱蔽的房頂上張望唆姐。 院中可真熱鬧,春花似錦廓八、人聲如沸奉芦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽声功。三九已至烦却,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間先巴,已是汗流浹背其爵。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伸蚯,地道東北人摩渺。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像剂邮,于是被迫代替她去往敵國和親摇幻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容