前 言
记得之前写过一篇文章,介绍怎么将amr音频转为wav格式,这个过程是没有问题的,转码产生的音频文件是可以正常播放的。但是,由于项目中的服务器智能播放比特率为64kbps的wav音频,而转码产生的wav音频比特率为128kbps,导致不可用。
wav音频
WAVE文件作为多媒体中使用的声波文件格式之一,它是以RIFF格式为标准的。RIFF是英文Resource Interchange File Format的缩写,每个WAVE文件的头四个字节便是“RIFF”。
技术选型
由于转码产生的音频比特率不符合要求,那么就需要修改wav文件的比特率为64kbps了。该音频的采样率为16k,改为8k就可以。
构建一个wav文件头
wav文件头是有一定结构的,占用44个字节记录文件的信息。
public class PcmWavHead {
/**
* 0-3个字节,WAV固定为RIFF,RIFF是windows下的一种常用多媒体格式存储标准
*/
public String ChunkID="RIFF";
/**
* 4-7个字节,文件长度
*/
public int ChunkSize=0;
/**
* 8-11个字节,WAV文件格式标志
*/
public String Format="WAVE";
/**
* 12-15个字节,"fmt "标志
*/
public String SubChunk1ID="fmt ";
/**
* 16-19个字节 块大小,初始化为一个数,但是实际上的大小决定,计算的公式为 (HeadSize + DataSize - 8) = (44-8+DataSize) 字节
*/
public int SubChunk1Size=0x10; //
/**
* 20-21个字节,格式类别(0x01H为PCM形式的声音数据)
*/
public int AudioFormat=0x1; //
/**
* 22-23个字节, 通道数,单声道为1,双声道为2
*/
public int NumChannels=1; //
/**
* 24-27个字节 采样率(每秒样本数),表示每个通道的播放速度,8000 | 6000 | 11025 | 16000
*/
public int SampleRate=16000; //
/**
* 28-31个字节 波形音频数据传送速率,其值Channels×SamplesPerSec×BitsPerSample/8 每秒字节数
*/
public int ByteRate=32000; //32Kbps
/**
* 32-33个字节,数据块的调整数(按字节算的),其值为Channels×BitsPerSample/8
*/
public int BlockAlign=2; //
/**
* 34-35个字节 每样本的数据位数,表示每个声道中各个样本的数据位数。如果有多个声道,对每个声道而言,样本大小都一样。量化比特数: 8 | 16
*/
public int BitsPerSample=16; //
/**
* 36-39个字节,数据标记符"data",前11个是用于鉴别文件的头部信息
*/
public String DataTag="data"; //
/**
* 40-43个字节,语音数据的长度(文长-44)
*/
public int DataSize=0; //
/**
* 固定头信息文长44个字节
*/
public static int HeadSize=44;
}
读取wav的头结构
/**
* 从data流中读取文件头
*
* @param dis 输入数据流
* @return void
*/
public void readHead(DataInputStream dis) {
byte[] byteHeads= new byte[HeadSize];
try {
dis.readFully(byteHeads, 0, HeadSize);//读取pcm文件的前44个字节,保存到byteHeads当中
} catch (IOException e) {
logger.error(ExceptionTool.getExceptionStacksMessage(e));
}
readHead(byteHeads);
}
/**
* 从文件流中读取文件头
*
* @param fis 输入数据流
* @return void
*/
public void readHead(FileInputStream fis) {
byte[] byteHeads= new byte[HeadSize];
try {
fis.read(byteHeads, 0, HeadSize);
} catch (IOException e) {
e.printStackTrace();
}
readHead(byteHeads);
}
/**
* 将保存了头信息的44个字节,装载到类当中
*
* @param pcmHead 保存了pcm文件的前44个字节的数组
* @return void
*/
public void readHead(byte[] pcmHead)
{
//11个关键文件头字段
ChunkID = SpeexUtil.readString(pcmHead, 0, 4);
ChunkSize = SpeexUtil.readInt(pcmHead, 4);
Format = SpeexUtil.readString(pcmHead, 8, 4);
SubChunk1ID = SpeexUtil.readString(pcmHead, 12, 4);
SubChunk1Size = SpeexUtil.readInt(pcmHead, 16);
AudioFormat = SpeexUtil.readShort(pcmHead, 20);//
NumChannels = SpeexUtil.readShort(pcmHead, 22);//
SampleRate = SpeexUtil.readInt(pcmHead, 24);
ByteRate = SpeexUtil.readInt(pcmHead, 28);
BlockAlign = SpeexUtil.readShort(pcmHead, 32);//
BitsPerSample = SpeexUtil.readShort(pcmHead, 34);//
DataTag = SpeexUtil.readString(pcmHead, 36, 4);
DataSize = SpeexUtil.readInt(pcmHead, 40);
}
SpeexUtil中的方法如下:
/**
* 使用Int的方式读取数据流中数据,保存在byte数组当中
*
* @param data 用于保存读取信息的byte数组
* @param offset 偏置第几个字节
* @return int
*/
public static int readInt(final byte[] data, final int offset) {
/*
* no 0xff on the last one to keep the sign
*/
return (data[offset] & 0xff) | ((data[offset + 1] & 0xff) << 8)
| ((data[offset + 2] & 0xff) << 16) | (data[offset + 3] << 24);
}
/**
* 使用short的方式读取数据流中数据,保存在byte数组当中
*
* @param data 用于保存读取信息的byte数组
* @param offset 偏置第几个字节
* @return short
*/
public static int readShort(final byte[] data, final int offset) {
/*
* no 0xff on the last one to keep the sign
*/
return (data[offset] & 0xff) | (data[offset + 1] << 8);
}
/**
* 使用String的方式读取数据流中数据,保存在byte数组当中
*
* @param data 用于保存读取信息的byte数组
* @param offset 偏置第几个字节
* @return String
*/
public static String readString(final byte[] data, final int offset, final int len) {
return new String(data, offset, len);
}
读取文件字节数组
int totalpcmsize=pcmWavHead.DataSize;
// 跳过44个字节
int wavLen= (int) (totalpcmsize)/2; // 输入数据长度
int rawLen= wavLen/2; // 输出数据长度
short[] wavData=new short[wavLen]; // 输入数据数组
short[] rawData = new short[ rawLen] ; // 输出数据数组
byte[] buffer1=new byte[totalpcmsize];
dis.read(buffer1, 0, totalpcmsize);
//将字节拼成short
wavData=byteArray2ShortArray(buffer1, wavLen);
将字节数组转为short数组
/**
* 将byte型的数组,转换为short型的数组
*
* @param data
* @param items
* @return short[]
*/
public static short[] byteArray2ShortArray(byte[] data, int items) {
short[] retVal =new short[items];
for (int i =0; i < retVal.length; i++)
retVal[i] = (short) ((data[i *2]&0xff) | (data[i *2+1]&0xff) <<8);
return retVal;
}
修改文件采样率为8k
采样率变了之后,数据大小也会跟着变
计算 Pcm的时长:时长=数据量byte/(采样率*(采样位数/8)*声道数)
pcmWavHead.SampleRate=8000;
pcmWavHead.DataSize=wavLen;// (int) m_duration * (pcmWavHead.SampleRate*(16/8)*1);
byte[] headBytes=pcmWavHead.buildHeader();
//将头文件写入新wav文件
outputStream.write(headBytes, 0, PcmWavHead.HeadSize);
/**
* 根据当前类的实例,构建的一个pcm头,并以byte数组的形式返回
*
* @param
* @return byte[] byte数组的形式返回的头信息
*/
public byte[] buildHeader() {
byte[] header = new byte[HeadSize];
SpeexUtil.writeString(header, 0, "RIFF");
SpeexUtil.writeInt (header, 4, DataSize+HeadSize-8);//总体长度减去8
SpeexUtil.writeString(header, 8, "WAVE");
SpeexUtil.writeString(header, 12, "fmt ");
SpeexUtil.writeInt (header, 16, 0x10); // Size of format chunk
SpeexUtil.writeShort (header, 20, (short) 0x01); // Format tag: PCM
SpeexUtil.writeShort (header, 22, (short) NumChannels); // Number of channels
SpeexUtil.writeInt (header, 24, SampleRate); // Sampling frequency
SpeexUtil.writeInt (header, 28, SampleRate*NumChannels * (BitsPerSample/8)); // Average bytes per second
SpeexUtil.writeShort (header, 32, (short) NumChannels * (BitsPerSample/8)); // Blocksize of data
SpeexUtil.writeShort (header, 34, (short) BitsPerSample); // Bits per sample
SpeexUtil.writeString(header, 36, "data");
SpeexUtil.writeInt (header, 40, DataSize); // Data Size
return header;
}
抽取数据转为8k
//抽取数据 将16k转为 8K
for(int i=0;i<rawLen-1;i++){
rawData[i]=(short) ((wavData[i*2] + wavData[(i+1)*2])/2);
}
for (int i = 0; i < rawLen-1; i++) {
//写数据,不减1 数组会越界why?
writeShort(outputStream, rawData[i]);
}
总 结
转化wav音频的转化率还是比较复杂的,涉及到底层数组的的操作。原理就是将文件头保存的采样率16k改为8k,再计算文件的大小,并将文件数据转化为8k。这个过程还是需要对wav文件的结构熟悉。总之,这个部分还是很复杂的,我现在还只是知道个大概。今天将这个知识点写出来,就是想将自己的思考整理一下吧。