Android录制wav(暂停,再录制,多段wav音频拼接)& 获取wav音频时长(根据头文件)

需求

录制音频文件,需要支持暂停,再录制。
暂停状态下可播放录音,播放完毕后能继续录制!


设计稿

最初的我

由于之前做过录音,我采用的 MediaRecorder 录制的 MPEG_4 格式的音频文件。

绕不过的问题
暂停 / 继续录制,MediaRecorder 的 pause方法& resume方法
需要 Android 7.0 以上版本才支持

Build.VERSION.SDK_INT >= Build.VERSION_CODES.N

低版本的兼容必须要做 多个音频文件拼接 处理。

ps:就算可以不用兼容低版本,还是有问题的;
暂停状态的文件不是完整文件,无法播放!
需要调用stop方法后才会生成完整文件,才可以播放。
问题就在这,调用了stop方法后,就不能再通过resume方法进行继续录制!

问题又回到了原点,需要拼接 多段音频文件

拼接过程探索

文件格式的选取

MPEG_4 我没有找到详细的文件格式,拼接的时候需要跳过 文件头 header信息
否则拼接的文件是无法播放的,是损坏掉的。

经过一系列的尝试和查询资料,最后确定要用 wav 格式。

拼接过程

这篇文章有一个拼接wav文件的工具类,亲测可用!

记录其代码如下(包括作者信息):

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();
  }

  /**
   * 解析头部,并获得文件指针指向数据开始位置的InputStreram,记得使用后需要关闭
   */
  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转化为byte[]
   */
  private static byte[] intToByteArray(int data) {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array();
  }

  /**
   * 将short转化为byte[]
   */
  private static byte[] shortToByteArray(short data) {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array();
  }

  /**
   * 将byte[]转化为short
   */
  private static short byteArrayToShort(byte[] b) {
    return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort();
  }

  /**
   * 将byte[]转化为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;
  }
}

作者:猿某某
链接:https://www.jianshu.com/p/86edb2422b21
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

录制过程

很遗憾的是 MediaRecorder 不支持 wav 文件的录制,
需要用 AudioRecord ,而且需要自己拼 wav 的头文件。
刚好在查询资料的时候,遇到了一个Github的Demo,亲测可用!需要自己按需优化!

支持暂停,续录,多段拼接wav及播放录音功能

将两者结合,使用起来是没有问题的。亲测可用!
我在开始录制的时候,使用集合记录了path,
在结束录制的时候拼接了集合中记录的wav文件,播放是没有问题的。

// start
voicePaths.add(new File(voicePath));

// stop
WavMergeUtil.mergeWav(voicePaths,new File(WavApp.rootPath + "hahh" + ".wav"));

最后的我

推荐上述工具类&库,希望可以帮助到同样困惑的童鞋!


获取 wav 音频时长(2018.12.4)

工具类中没有获取 wav 音频时长 的方法,所以添加如下:
在 WavMergeUtil 中,解析 wav 头部文件,并获取。

  /**
     * 根据本地文件地址获取wav音频时长 
     *
     */
    public static long getWavLength(String filePath) {
        byte[] wavdata = getBytes(filePath);
        if (wavdata != null && wavdata.length > 44) {
            int byteRate = byteArrayToInt(wavdata, 28, 31);
            int waveSize = byteArrayToInt(wavdata, 40, 43);
            return waveSize * 1000 / byteRate;
        }
        return 0;
    }

    /**
     * file 2 byte数组
     */
    private static byte[] getBytes(String filePath) {
        byte[] buffer = null;
        try {
            File file = new File(filePath);
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);
            byte[] b = new byte[1000];
            int n;
            while ((n = fis.read(b)) != -1) {
                bos.write(b, 0, n);
            }
            fis.close();
            bos.close();
            buffer = bos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return buffer;
    }

    /**
     * 将byte[]转化为int
     */
    private static int byteArrayToInt(byte[] b, int start, int end) {
        return ByteBuffer.wrap(b, start, end).order(ByteOrder.LITTLE_ENDIAN).getInt();
    }

附 wav 头部文件结构图:


wav 头部文件结构图

区块结构代码:

 private byte[] wavFileHeader(long totalAudioLen, long totalDataLen, long longSampleRate,
                                 int channels, long byteRate, byte bitsPerSample) {
        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF/WAVE header
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        header[12] = 'f'; // 'fmt ' chunk
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        header[20] = 1; // format = 1
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        header[32] = (byte) (channels * (bitsPerSample / 8)); //
        // block align
        header[33] = 0;
        header[34] = bitsPerSample; // bits per sample
        header[35] = 0;
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        return header;
    }

data区块 size 是 所有音频数据的长度,
fmt区块 ByteRate 是 每秒数据字节数,
所以我们知道,音频时长的算法是(size / ByteRate),如上工具类代码。

获取 wav 音频时长(2019.1.28)

  /**
     * 根据本地文件地址获取wav音频时长 
     *
     */
    public static long getWavLength(String filePath) {
        byte[] wavdata = getBytes(filePath);
        if (wavdata != null && wavdata.length > 44) {
            int byteRate = byteArrayToInt(wavdata, 28, 31);
            int waveSize = byteArrayToInt(wavdata, 40, 43);
            return waveSize * 1000 / byteRate;
        }
        return 0;
    }

采用如下方法,获取到的数据有时会为负值!修改如下就好了!:

    /**
     * 根据本地文件地址获取wav音频时长
     */
    public static long getWavLength(String filePath) {
        byte[] wavdata = getBytes(filePath);
        if (wavdata != null && wavdata.length > 44) {
            int byteRate = byteArrayToInt(wavdata, 28, 31);
            long waveSize = (new File(filePath).length() - 44);
            return waveSize * 1000 / byteRate;
        }
        return 0;
    }

百思不得其解(一个是从文件头获取数据,一个是直接获取文件大小,应该是没有区别才对),打断点数据如下,也没看出来问题,莫非是数据类型由 int 改为了 long 吗?

断点数据

啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊 !!!!!!
就是由 int 改为了 long ,解决了这个问题!!!

int 值占32位的时候,最大可以赋值为:2147483647,也就是 10 位 !
而断点数据是10777844,8位,没超阈值,但是我最后算法中

waveSize * 1000 / byteRate;

乘1000之后,就是11位数了,超过了 int 最大值,所以得到了负值!!!

所以,计算 wav 格式音频时长的最终写法如下,修改 int 类型为 long 即可:

    /**
     * 根据本地文件地址获取wav音频时长
     */
    public static long getWavLength(String filePath) {
        byte[] wavdata = getBytes(filePath);
        if (wavdata != null && wavdata.length > 44) {
            long byteRate = byteArrayToInt(wavdata, 28, 31);
            long waveSize = byteArrayToInt(wavdata, 40, 43);
            return waveSize * 1000 / byteRate;
        }
        return 0;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容