需求
录制音频文件,需要支持暂停,再录制。
暂停状态下可播放录音,播放完毕后能继续录制!
最初的我
由于之前做过录音,我采用的 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 头部文件结构图:
区块结构代码:
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;
}