前 言
记得之前写过一篇文章,介绍怎么将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);//将字节拼成shortwavData=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);//总体长度减去8SpeexUtil.writeString(header, 8, "WAVE");SpeexUtil.writeString(header, 12, "fmt ");SpeexUtil.writeInt (header, 16, 0x10);// Size of format chunkSpeexUtil.writeShort (header, 20, (short) 0x01);// Format tag: PCMSpeexUtil.writeShort (header, 22, (short) NumChannels);// Number of channelsSpeexUtil.writeInt (header, 24, SampleRate); // Sampling frequencySpeexUtil.writeInt (header, 28, SampleRate*NumChannels * (BitsPerSample/8)); // Average bytes per secondSpeexUtil.writeShort (header, 32, (short) NumChannels * (BitsPerSample/8)); // Blocksize of dataSpeexUtil.writeShort (header, 34, (short) BitsPerSample); // Bits per sampleSpeexUtil.writeString(header, 36, "data");SpeexUtil.writeInt (header, 40, DataSize); // Data Sizereturn header;}
抽取数据转为8k
//抽取数据 将16k转为 8Kfor(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文件的结构熟悉。总之,这个部分还是很复杂的,我现在还只是知道个大概。今天将这个知识点写出来,就是想将自己的思考整理一下吧。
如果觉得《修改wav格式音频比特率的标准方法》对你有帮助,请点赞、收藏,并留下你的观点哦!