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

你可能感兴趣的:(Android录制wav(暂停,再录制,多段wav音频拼接)& 获取wav音频时长(根据头文件))