概述
Android 在使用 AudioRecord 录音时,采集的是 PCM 数据,有时候在录完后向听下录音效果,还需要拉到电脑端用 AU 播放,手机端没法直接播放 PCM 数据。因此封装了一个 WavFile 在 PCM 前面增加 Wav 头,生成 wav 文件,可以在手机上直接播放。文件头信息见:《Audio WAV文件头格式》
使用方法如下:
String wavSavePath = "/sdcard/Alan/audio/record.wav";
// 指定 PCM 格式(跟 AudioRecord 配置一致),内部自动生成 wav 头信息
WavFile.HeadInfo headInfo = WavFile.HeadInfo.build()
.setSampleRate(44100)
.setChannelCount(1)
.setBytePerSample(2);
WavFile wavFile = new WavFile(wavSavePath, headInfo);
// 录制过程中从 AudioRecord 读到 PCM 数据后写入 wavFile
wavFile.write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());
// 录制结束关闭文件
wavFile.close();
完整 Demo 见 GitHub
WavFile 封装如下:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* 封装文件头尾 44 字节长度的 WavFile,支持对 WavFile 的读、写操作,
* 以 {@link #WavFile(String)} 方式创建时,为[读]模式,可以通过 {@link #getHeadInfo()} 获取 wav 文件的头信息
* 以 {@link #WavFile(String, HeadInfo)} 方式创建时,为[写]模式
*
* Author: AlanWang4523.
* Date: 2020/10/28 20:35.
* Mail: alanwang4523@gmail.com
*/
public class WavFile {
private HeadInfo mHeadInfo;
private RandomAccessFile mWavFile;
private int mAudioDataLenInBytes;
private volatile boolean isWriteMode;
private volatile boolean isClosed;
/**
* 构建一个 WavFile,该文件已存在,以读模式打开
* @param filePath wav file 文件路径
* @throws IOException IOException
*/
public WavFile(String filePath) throws IOException {
this(filePath, null);
}
/**
* 根据 HeadInfo 创建一个 WavFile,以写模式打开
* @param filePath wav file 文件路径
* @param wavHeaderInfo wavHeaderInfo
* @throws IOException IOException
*/
public WavFile(String filePath, HeadInfo wavHeaderInfo) throws IOException {
mWavFile = new RandomAccessFile(filePath, "rw");
if (wavHeaderInfo != null) {
mHeadInfo = wavHeaderInfo;
byte[] wavHeader = generateWavHeader(
wavHeaderInfo.getSampleRate(),
wavHeaderInfo.getChannelCount(),
wavHeaderInfo.getBytePerSample() * 8,
0);
mWavFile.write(wavHeader);
isWriteMode = true;
} else {
mHeadInfo = getWavHeader(mWavFile);
isWriteMode = false;
}
mWavFile.seek(44);
mAudioDataLenInBytes = 0;
isClosed = false;
}
/**
* 获取 wav 头信息
* @return HeadInfo
*/
public HeadInfo getHeadInfo() {
return mHeadInfo;
}
/**
* 读取 PCM 数据
* @param data pcm 数据存放的位置
* @param off offset
* @param len 想要读取的长度,单位:字节
* @return 读取的长度,单位:字节
* @throws IOException IOException
*/
public int read(byte[] data, int off, int len) throws IOException {
if (isWriteMode) {
throw new IOException("The current file is not read mode.");
}
if (isClosed) {
return 0;
}
return mWavFile.read(data, off, len);
}
/**
* 写 PCM 数据
* @param data 音频数据
* @param offset offset
* @param len 数据长度,单位:字节
*/
public void write(byte[] data, int offset, int len) throws IOException {
if (!isWriteMode) {
throw new IOException("The current file is not write mode.");
}
if (isClosed || data == null || len <= 0) {
return;
}
mWavFile.write(data, offset, len);
mAudioDataLenInBytes += len;
}
/**
* 更新 wav 文件头信息,并关闭文件
* @throws IOException IOException
*/
public void close() throws IOException {
if (isClosed) {
return;
}
isClosed = true;
// 如果是写入模式,则更新文件头中的数据长度信息
if (isWriteMode) {
int totalFileLenIncludeHeader = mAudioDataLenInBytes + 44;
//更新wav文件头04H— 08H的数据长度:该长度 = 文件总长 - 8
mWavFile.seek(4);
mWavFile.write(int2ByteArray(totalFileLenIncludeHeader - 8));
//更新wav文件头28H— 2CH,实际PCM采样数据长度
mWavFile.seek(40);
mWavFile.write(int2ByteArray(totalFileLenIncludeHeader - 44));
}
mWavFile.close();
}
/**
* 生成 44 字节 WAV 文件头
* @param sampleRate 采样率,如 44100
* @param channels 通道数,如立体声为2
* @param bitsPerSample 采样精度,即每个采样所占数据位数,如 16,表示每个采样 16bit 数据,即 2 个字节
* @param audioDataLenInBytes 音频数据长度
* @return 44 字节 WAV 头信息
*/
private byte[] generateWavHeader(int sampleRate, int channels, int bitsPerSample, int audioDataLenInBytes) {
if (bitsPerSample != 16 && bitsPerSample != 32) {
throw new IllegalArgumentException("The bitsPerSample is not 16 or 32!");
}
if (audioDataLenInBytes < 0) {
throw new IllegalArgumentException("Audio data len could not be negative!");
}
byte[] wavHeader = new byte[44];
// 这个长度不包括"RIFF"标志(4字节)和文件长度本身所占字节(4字节),即该长度等于整个 Wav文件长度(包含44字节头) - 8
// 也等于纯音频数据的长度 + 36
int ckTotalSize = 36 + audioDataLenInBytes;
// 生成文件头默认纯音频数据长度为 0
int audioDataLen = audioDataLenInBytes;
// 音频数据传送速率, 单位是字节。其值为采样率×每次采样大小。播放软件利用此值可以估计缓冲区的大小。
// bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
int bytePerSecond = sampleRate * (bitsPerSample / 8) * channels;
//ckid:4字节 RIFF 标志,大写
wavHeader[0]
= 'R';
wavHeader[1]
= 'I';
wavHeader[2]
= 'F';
wavHeader[3]
= 'F';
//cksize:4字节文件长度,这个长度不包括"RIFF"标志(4字节)和文件长度本身所占字节(4字节),即该长度等于整个文件长度 - 8
wavHeader[4]
= (byte)(ckTotalSize & 0xff);
wavHeader[5]
= (byte)((ckTotalSize >> 8) & 0xff);
wavHeader[6]
= (byte)((ckTotalSize >> 16) & 0xff);
wavHeader[7]
= (byte)((ckTotalSize >> 24) & 0xff);
//fcc type:4字节 "WAVE" 类型块标识, 大写
wavHeader[8]
= 'W';
wavHeader[9]
= 'A';
wavHeader[10] = 'V';
wavHeader[11] = 'E';
//ckid:4字节 表示"fmt" chunk的开始,此块中包括文件内部格式信息,小写, 最后一个字符是空格
wavHeader[12] = 'f';
wavHeader[13] = 'm';
wavHeader[14] = 't';
wavHeader[15] = ' ';
//cksize:4字节,文件内部格式信息数据的大小,过滤字节(一般为00000010H)
wavHeader[16] = 0x10;
wavHeader[17] = 0;
wavHeader[18] = 0;
wavHeader[19] = 0;
//FormatTag:2字节,音频数据的编码方式,1:表示是PCM 编码
wavHeader[20] = 1;
wavHeader[21] = 0;
//Channels:2字节,声道数,单声道为1,双声道为2
wavHeader[22] = (byte) channels;
wavHeader[23] = 0;
//SamplesPerSec:4字节,采样率,如44100
wavHeader[24] = (byte)(sampleRate & 0xff);
wavHeader[25] = (byte)((sampleRate >> 8) & 0xff);
wavHeader[26] = (byte)((sampleRate >> 16) & 0xff);
wavHeader[27] = (byte)((sampleRate >> 24) & 0xff);
//BytesPerSec:4字节,音频数据传送速率, 单位是字节。其值为采样率×每次采样大小。播放软件利用此值可以估计缓冲区的大小;
//bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
wavHeader[28] = (byte)(bytePerSecond & 0xff);
wavHeader[29] = (byte)((bytePerSecond >> 8) & 0xff);
wavHeader[30] = (byte)((bytePerSecond >> 16) & 0xff);
wavHeader[31] = (byte)((bytePerSecond >> 24) & 0xff);
//BlockAlign:2字节,每次采样的大小 = 采样精度*声道数/8(单位是字节); 这也是字节对齐的最小单位, 譬如 16bit 立体声在这里的值是 4 字节。
//播放软件需要一次处理多个该值大小的字节数据,以便将其值用于缓冲区的调整
wavHeader[32] = (byte)(bitsPerSample * channels / 8);
wavHeader[33] = 0;
//BitsPerSample:2字节,每个声道的采样精度; 譬如 16bit 在这里的值就是16。如果有多个声道,则每个声道的采样精度大小都一样的;
wavHeader[34] = (byte) bitsPerSample;
wavHeader[35] = 0;
//ckid:4字节,数据标志符(data),表示 "data" chunk的开始。此块中包含音频数据,小写;
wavHeader[36] = 'd';
wavHeader[37] = 'a';
wavHeader[38] = 't';
wavHeader[39] = 'a';
//cksize:音频数据的长度,4字节,audioDataLen = ckSize - 36 = fileLenIncludeHeader - 44
wavHeader[40] = (byte)(audioDataLen & 0xff);
wavHeader[41] = (byte)((audioDataLen >> 8) & 0xff);
wavHeader[42] = (byte)((audioDataLen >> 16) & 0xff);
wavHeader[43] = (byte)((audioDataLen >> 24) & 0xff);
return wavHeader;
}
/**
* 从 wav 文件中获取头信息
* 注意:必须是 44 字节头的 wav 文件
* @param randomAccessFile wav 文件
* @return HeadInfo
* @throws IOException IOException
*/
private HeadInfo getWavHeader(RandomAccessFile randomAccessFile) throws IOException {
//读取channelCount,第22~23位
randomAccessFile.seek(22);
byte[] channelCountArray = new byte[2];
randomAccessFile.read(channelCountArray);
int channelCount = byteArray2Short(channelCountArray);
//读取sampleRate,第24~27位
randomAccessFile.seek(24);
byte[] sampleRateArray = new byte[4];
randomAccessFile.read(sampleRateArray);
int sampleRate = byteArray2Int(sampleRateArray);
//读取BitsPerSample,第34~35位
randomAccessFile.seek(34);
byte[] bitsPerSampleArray = new byte[2];
randomAccessFile.read(bitsPerSampleArray);
int bytePerSample = byteArray2Short(bitsPerSampleArray) / 8;
return HeadInfo.build().
setSampleRate(sampleRate).
setChannelCount(channelCount).
setBytePerSample(bytePerSample);
}
/**
* 将 byte 数组转成short
* @param b byte 数组
* @return 返回 short 数值
*/
private static short byteArray2Short(byte[] b) {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort();
}
/**
* 将整型转成 byte 数组
* @param data 要转换的数字
* @return byte 数组
*/
private static byte[] int2ByteArray(int data) {
return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array();
}
/**
* 将 byte 数组转成整型
* @param b byte 数组
* @return int 数值
*/
private static int byteArray2Int(byte[] b) {
return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
public static class HeadInfo {
/**
* 采样率
*/
private int sampleRate;
/**
* 通道数
*/
private int channelCount;
/**
* 每个采样点的大小,
* short 型 PCM 是 2 字节
* float 型 PCM 是 4 字节
* 单位:字节
*/
private int bytePerSample = 2;
/**
* 构造 HeadInfo
* @return HeadInfo 实例
*/
public static HeadInfo build() {
return new HeadInfo();
}
/**
* 获取音频采样率,如:44100、48000
* @return 设置参数后的 HeadInfo 实例
*/
public int getSampleRate() {
return sampleRate;
}
/**
* 设置音频采样率
* @param sampleRate 采样率,如:44100、48000
* @return 设置参数后的 HeadInfo 实例
*/
public HeadInfo setSampleRate(int sampleRate) {
this.sampleRate = sampleRate;
return this;
}
/**
* 获取音频通道数,如:1、2
* @return 设置参数后的 HeadInfo 实例
*/
public int getChannelCount() {
return channelCount;
}
/**
* 设置音频通道数
* @param channelCount 音频通道数
* @return 设置参数后的 HeadInfo 实例
*/
public HeadInfo setChannelCount(int channelCount) {
this.channelCount = channelCount;
return this;
}
/**
* 获取每个采样点大小,单位:字节
* short 型 PCM 是 2 字节
* float 型 PCM 是 4 字节
* @return 每个采样点占的字节数
*/
public int getBytePerSample() {
return bytePerSample;
}
/**
* 设置每个采样占的字节数
* @param bytePerSample bytePerSample
* @return 设置参数后的 HeadInfo 实例
*/
public HeadInfo setBytePerSample(int bytePerSample) {
this.bytePerSample = bytePerSample;
return this;
}
}
}
完整 Demo 见 GitHub
最后
以上就是美满荷花为你收集整理的Android Audio Wav 文件读写操作的封装的全部内容,希望文章能够帮你解决Android Audio Wav 文件读写操作的封装所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复