目录
1、文件整体结构
1.1 RIFF Chunk块
1.2 Format Chunk区块
1.3 DATA块
1.4 文件示例分析
2、Android上Wav录制
2.1 首先初始化AudioRecord(忽略权限相关代码):
2.2 启动录制
2.3 格式转换(Wav)
3、参考文章
WAV文件的数据体区块一般由3个区块组成:RIFF Chunk、Format Chunk和Data Chunk。如上图三个不同颜色区域。
RIFF数据块长度为12字节,共有三种码段。如下表所示。
名称 | 偏移地址 | 字节数 | 内容 |
---|---|---|---|
ID | 0x00 | 4 | RIFF (0x52494646H) |
Size | 0x04 | 4 | fileSize - 8 |
Type | 0x08 | 4 | WAVE(0x57415645H) |
- RIFF Chunk类型数据块以RIFF的ID部分为标识,说明数据块类型;
- Size是整个文件的长度减去ID和Size的长度,表征包含Type字段在内的数据块长度;
- Type是WAVE表示后面需要Format和Data两个子区块。
- 此类型数据块以“fmt”的ID为标识,说明数据块类型;
- Size表示该区块数据不包含ID和Size的长度;
- AudioFormat表示Data区块存储的音频数据的格式,PCM音频数据的值为1;
- NumChannels表示音频数据的声道数,其中1表示单声道,2表示双声道;
- SampleRate表示音频数据的采样率;
- ByteRate每秒数据字节数,计算公式为:ByteRate = SampleRate × NumChannels × BitsPerSample / 8
- BlockAlign每个采样所需的字节数,计算公式为NumChannels*BitsPerSample/8;
- BitsPerSample每个采样存储的bit数,其中8表示8bit,16表示16bit,32表示32bit。
该区块标识wav音频文件实际存储的数据
名称 | 偏移地址 | 字节数 | 内容 |
---|---|---|---|
ID | 0x00 | 4 | ‘data’ (0X64617461H) |
Size | 0x04 | 4 | -- |
Data | 0x08 | -- | 实际音频数据 |
描述WAVE文件的基本单元是"sample"(也就是采样的样本),一个sample代表采样一次得到的数据;因此如果用44KHz采样,将在一秒中得到44000个sample;每个sample可以用8位、24位,甚至32位表示(位数没有限制,只要是8的整数倍即可),位数越高,音频质量越好。
此处有一个值得注意的细节,8位代表无符号的数值,而16位或16位以上代表有符号的数值;例如,如果有一个10bit的样本,由于sample位数要求是8的倍数,我们就需要把它填充到16位。16位中:0-5位补0,6-15位是原始的10bit数据。这就是左补零对齐原则。
上述只是单声道,如果要处理多声道,就需要在任意给定时刻给出多个sameple。例如,在多声道中,给出某一时刻,我们需要分辨出哪些sample是左声道的,哪些sample是右声道的。因此我们需要一次读写两个sample。假如以44KHz取样立体声音频,我们需要一秒读写44*2 KHz的sample,给出公式:
每秒数据大小(字节)= 采样率 * 声道数 * sample比特数 / 8
处理多声道音频时,每个声道的样本是交叉存储的。我们把左右声道数据交叉存储在一起:先存储第一个sample的左声道数据,然后存储第一个sample的右声道数据。当一个设备需要重现声音时,它需要同时处理多个声道,一个sample中多个声道信息称为一个样本帧。
- Size表示音频数据的长度,不包含ID和Size数据段,且对于采样率为ByteRate的音频文件数据来说,其计算公式为:Size = ByteRate × seconds
- Data为实际存储的Wav音频文件数据。
下面是一个数据示例(wav文件):
用表格说明一下文件的格式:
起始地址 |
占用空间 |
本地址数字的含义 |
00H |
4byte |
RIFF,资源交换文件标志(52494646H)。 |
04H |
4byte |
从下一个地址开始到文件尾的总字节数。高位字节在后面,这里就是00012C24H,换成十进制是76836byte,算上这之前的8byte就正好76844byte了。 |
08H |
4byte |
WAVE,代表wav文件格式(57415645H)。 |
0CH |
4byte |
FMT ,波形格式标志(666D7420H)。 |
10H |
4byte |
00000010H,SubChunk1Siz标识第二个区块数据大小,不包含ID和Size的长度(24 - 8 = 16byte)。 |
14H |
2byte |
为1时表示线性PCM编码,大于1时表示有压缩的编码。这里是0001H。 |
16H |
2byte |
1为单声道,2为双声道,这里是0001H。 |
18H |
4byte |
采样频率,这里是00003E80H,也就是16000Hz。 |
1CH |
4byte |
Byte率=采样频率*音频通道数*每次采样得到的样本位数/8,00007D00H,也就是32000Byte/s=16000*1*16/2。 |
20H |
2byte |
块对齐=通道数*每次采样得到的样本位数/8,0002H,也就是2=1*16/8。 |
22H |
2byte |
样本数据位数,0010H即16,一个量化样本占2byte。 |
24H |
4byte |
data,一个标志而已(64617461H)。 |
28H |
4byte |
Wav文件实际音频数据所占的大小,这里是00012C00H即76800,再加上2CH就正好是76844,整个文件的大小。 |
2CH |
不定 |
量化数据。 |
private byte[] buffer;
private void init() {
int sampleRateInHz = 16000;
int channelConfig = AudioFormat.CHANNEL_IN_DEFAULT;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int recordMinBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
//指定 AudioRecord 缓冲区大小
buffer = new byte[recordMinBufferSize];
//根据录音参数构造AudioRecord实体对象
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConfig, audioFormat, recordMinBufferSize);
}
/**
* 开始录制
*/
public void start(String path, String name) {
if (audioRecord.getState() == AudioRecord.RECORDSTATE_STOPPED) {
recorderState = true;
audioRecord.startRecording();
new RecordThread(path, name).start();
} else {
Log.i(TAG, "start: " + audioRecord.getState());
}
}
录制线程:
private class RecordThread extends Thread {
private String cachePath;
private String name;
private String path;
public RecordThread(String path, String name) {
this.path = path;
this.name = name;
this.cachePath = path + "cache.pcm";
}
@Override
public void run() {
Log.i(TAG, "run: pcm目录=path" + cachePath);
deleteFile(cachePath);
File pcmFile = new File(path + "cache.pcm");
boolean file = createFile(pcmFile);
if (file)
Log.i(TAG, "run: 创建缓存文件成功:" + cachePath);
else {
Log.i(TAG, "run: 创建缓存文件失败:" + cachePath);
return;
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(cachePath);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (fos == null) {
Log.i(TAG, "run: 未找到缓存文件" + cachePath);
return;
}
//获取到的pcm数据就是buffer
int read;
while (recorderState && !isInterrupted()) {
read = audioRecord.read(buffer, 0, buffer.length);
if (AudioRecord.ERROR_INVALID_OPERATION != read) {
try {
fos.write(buffer);
Log.i(TAG, "run: 写录音数据->" + read);
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
PcmToWavUtil.getInstance().pcmToWav(cachePath, path + name);
}
}
录制完成后,会把得到的pcm原始数据转换为wav格式的音频,工具类如下:
public class PcmToWavUtil {
private int mBufferSize; //缓存的音频大小
private int mSampleRate = 44100;// 此处的值必须与录音时的采样率一致
private int mChannel = AudioFormat.CHANNEL_IN_STEREO; //立体声
private int mEncoding = AudioFormat.ENCODING_PCM_16BIT;
private static class SingleHolder {
static PcmToWavUtil mInstance = new PcmToWavUtil();
}
public static PcmToWavUtil getInstance() {
return SingleHolder.mInstance;
}
public PcmToWavUtil() {
mSampleRate = AudioRecordManager.sampleRateInHz;
mChannel = AudioRecordManager.channelConfig;
mEncoding = AudioRecordManager.audioFormat;
mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, mEncoding);
GFLog.d("AudioRecordUtil", "mChannel = "+mChannel +", mEncoding: "+ mEncoding+", mSampleRate:"+mSampleRate + ", mBufferSize = " + mBufferSize);
}
/**
* pcm文件转wav文件
*
* @param inFilename 源文件路径
* @param outFilename 目标文件路径
* @param deleteOrg 是否删除源文件
*/
public void pcmToWav(String inFilename, String outFilename, boolean deleteOrg) {
FileInputStream in;
FileOutputStream out;
long totalAudioLen;
long totalDataLen;
long longSampleRate = 16_000;
int channels = AudioFormat.CHANNEL_IN_DEFAULT;
//ByteRate = SampleRate × NumChannels × BitsPerSample / 8
long byteRate = (longSampleRate * channels * 16L) / 8;
byte[] data = new byte[mBufferSize];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.flush();
out.close();
if (deleteOrg) {
new File(inFilename).delete();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void pcmToWav(String inFilename, String outFilename) {
new Thread(new Runnable() {
@Override
public void run() {
pcmToWav(inFilename, outFilename, false);
}
}).start();
}
/**
* 加入wav文件头
*/
private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels, long byteRate)
throws IOException {
byte[] header = new byte[44];
// ChunkID, RIFF, 占4bytes(big)
header[0] = 'R';
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
// ChunkSize, pcmLen + 36, 占4bytes
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
// Format, WAVE, 占4bytes(big)
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
// Subchunk1ID, 'fmt ', 占4bytes(big)
header[12] = 'f'; // 'fmt ' chunk
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
// Subchunk1Size, 16, 占4bytes(Size表示该区块(fmt)数据不包含ID和Size的长度: 24 - 8 = 16)
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
// AudioFormat, pcm = 1, 占2bytes
header[20] = 1;
header[21] = 0;
// NumChannels, mono = 1, stereo = 2, 占2bytes
header[22] = (byte) channels;
header[23] = 0;
// SampleRate, 占4bytes
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
// ByteRate = SampleRate * NumChannels * BitsPerSample / 8, 占4bytes
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// BlockAlign = NumChannels * BitsPerSample / 8, 占2bytes
header[32] = (byte) (channels * 16 / 8);
header[33] = 0;
// BitsPerSample, 占2bytes
header[34] = 16;
header[35] = 0;
// Subhunk2ID, data, 占4bytes(big)
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
// Subchunk2Size, 占4bytes
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}
其中文件头的写入是重点,可参考上面的说明进行对比阅读。
1)Wav文件直观展示
2)wav文件格式分析 - Dsp Tian - 博客园 (cnblogs.com)