音视频学习系列第(三)篇---wav文件的存储和解析

音视频系列

什么是wav

wav是一种无损的音频文件格式,wav文件有两部分,第一部分是文件头,记录一些重要的参数信息,如音频的采样率,通道数,数据位宽,第二部分是数据部分,数据部分可以是PCM,也可以是其它的编码格式的数据

为什么要将音频存储wav格式

存储为该格式,音乐播放器可以通过读取wav头,识别出它是音频文件,从而进行播放。
因为后缀名是可以任意修改的,不能简单的通过后缀名来判断该文件是否是音频文件

wav与pcm的区别

pcm是一种未经压缩的编码方式
wav是一种无损的音频文件格式

wav文件结构说明

wav文件结构图.png

little
小端法,低位字节放在内存的低地址端
big
大端法,低位字节放在内存的高地址端

public final void writeInt(int v) throws IOException {
    out.write((v >>> 24) & 0xFF);
    out.write((v >>> 16) & 0xFF);
    out.write((v >>>  8) & 0xFF);
    out.write((v >>>  0) & 0xFF);
    incCount(4);
}

write(int)和writeInt(int)区别
write只写入最低的8位
writeInt会按大端法写

字段详细说明

WaveHeader代码

public class WavFileHeader {

  public static final int WAV_FILE_HEADER_SIZE = 44;
  public static final int WAV_CHUNKSIZE_EXCLUDE_DATA = 36;

  public static final int WAV_CHUNKSIZE_OFFSET = 4;
  public static final int WAV_SUB_CHUNKSIZE1_OFFSET = 16;
  public static final int WAV_SUB_CHUNKSIZE2_OFFSET = 40;

  public String mChunkID="RIFF";
  public int mChunkSize=0;
  public String mFormat="WAVE";

  public String mSubChunk1ID="fmt ";
  public int mSubChunk1Size = 16;
  public short mAudioFormat = 1;
  public short mNumChannel = 1;
  public int mSampleRate = 8000;
  public int mByteRate = 0;
  public short mBlockAlign = 0;
  public short mBitsPerSample = 8;

  public String mSubChunk2ID = "data";
  public int mSubChunk2Size = 0;


  public WavFileHeader(){

  }

  public WavFileHeader(int sampleRateInHz, int channels, int bitsPerSample){
    mSampleRate = sampleRateInHz;
    mNumChannel = (short) channels;
    mBitsPerSample = (short) bitsPerSample;
    mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8;
    mBlockAlign = (short) (mNumChannel * mBitsPerSample / 8);
  }
}

将录音存储为wav文件

public class WavFileWriter {

private static final String TAG = "WavFileWriter";

private String mFilePath;
private int mDataSize = 0;
private DataOutputStream dos;


/**
 *
 * @param filePath
 * @param sampleRateInHz  采样率 44100
 * @param channels        声道数  1单声道  2双声道
 * @param bitsPerSample   每个样点对应的位数  16
 * @return
 */
public boolean openFile(String filePath, int sampleRateInHz, int channels, int bitsPerSample) {
    if (dos != null) {
        closeFile();
    }

    mFilePath = filePath;
    try {
        dos = new DataOutputStream(new FileOutputStream(mFilePath));
        return writeHeader(sampleRateInHz, channels, bitsPerSample);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return false;
    }
}


public boolean closeFile() {
    boolean result=false;
    if (dos != null) {
        try {
            result=writeDataSize();
            dos.close();
            dos=null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return result;
}

public boolean writeData(byte[] buffer, int offset, int count) {
    if (dos == null) {
        return false;
    }
    try {
        dos.write(buffer, offset, count);
        mDataSize += count;
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
    return true;
}

/**
 * 将一些需要计算出来的字段重新赋值
 * mChunkSize  位置4-8,值=36+原始音频数据大小
 * mSubChunk1Size  固定值16
 * mSubChunk2Size  位置40-44  值=原始音频数据大小
 */
private boolean writeDataSize() {
    if (dos == null) {
        return false;
    }
    try {
        RandomAccessFile waveAccessFile = new RandomAccessFile(mFilePath, "rw");
        waveAccessFile.seek(WavFileHeader.WAV_CHUNKSIZE_OFFSET);
        waveAccessFile.write(intToByteArray(WavFileHeader.WAV_CHUNKSIZE_EXCLUDE_DATA + mDataSize), 0, 4);
        waveAccessFile.seek(WavFileHeader.WAV_SUB_CHUNKSIZE2_OFFSET);
        waveAccessFile.write(intToByteArray(mDataSize), 0, 4);
        waveAccessFile.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return false;
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
    return true;
}

private boolean writeHeader(int sampleRateInHz, int channels, int bitsPerSample) {
    if (dos == null) {
        return false;
    }

    WavFileHeader header = new WavFileHeader(sampleRateInHz, channels, bitsPerSample);

    //按照wav文件结构依次写入
    try {
        dos.writeBytes(header.mChunkID);
        //这里不直接用writeInt的原因是它采用的大端法存储
        dos.write(intToByteArray(header.mChunkSize), 0, 4);
        dos.writeBytes(header.mFormat);
        dos.writeBytes(header.mSubChunk1ID);
        dos.write(intToByteArray(header.mSubChunk1Size), 0, 4);
        dos.write(shortToByteArray(header.mAudioFormat), 0, 2);
        dos.write(shortToByteArray(header.mNumChannel), 0, 2);
        dos.write(intToByteArray(header.mSampleRate), 0, 4);
        dos.write(intToByteArray(header.mByteRate), 0, 4);
        dos.write(shortToByteArray(header.mBlockAlign), 0, 2);
        dos.write(shortToByteArray(header.mBitsPerSample), 0, 2);
        dos.writeBytes(header.mSubChunk2ID);
        dos.write(intToByteArray(header.mSubChunk2Size), 0, 4);
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
    return true;
}


private static byte[] intToByteArray(int data) {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(data).array();
}

private static byte[] shortToByteArray(short data) {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(data).array();
}
}

解析wav文件并播放

public class WavFileReader {
private static final String TAG="WavFileReader";

private DataInputStream dis;
private WavFileHeader mWavFileHeader;


public WavFileHeader getWavFileHeader(){
    return mWavFileHeader;
}

public boolean openFile(String filePath){
    if(dis!=null){
        closeFile();
    }
    try {
        dis=new DataInputStream(new FileInputStream(filePath));
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }

    return readHeader();
}

public void closeFile(){
    if(dis!=null){
        try {
            dis.close();
            dis=null;
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

public int readData(byte[] buffer, int offset, int count) {
    if (dis == null || mWavFileHeader == null) {
        return -1;
    }

    try {
        int nbytes = dis.read(buffer, offset, count);
        if (nbytes == -1) {
            return 0;
        }
        return nbytes;
    } catch (IOException e) {
        e.printStackTrace();
    }

    return -1;
}


/**
 *read和read(byte b[])
 * read每次读取一个字节,返回0-255的int字节值
 * read(byte b[])读取一定数量的字节,返回实际读取的字节的数量
 */
private boolean readHeader(){
    if(dis==null){
        return false;
    }

    WavFileHeader header=new WavFileHeader();
    byte[] intValue = new byte[4];
    byte[] shortValue = new byte[2];

    try {
        header.mChunkID = "" + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte();
        Log.d(TAG, "Read file chunkID:" + header.mChunkID);

        dis.read(intValue);
        header.mChunkSize=byteArrayToInt(intValue);
        Log.d(TAG, "Read file chunkSize:" + header.mChunkSize);

        header.mFormat = "" + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte();
        Log.d(TAG, "Read file format:" + header.mFormat);

        header.mSubChunk1ID = "" + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte();
        Log.d(TAG, "Read fmt chunkID:" + header.mSubChunk1ID);

        dis.read(intValue);
        header.mSubChunk1Size = byteArrayToInt(intValue);
        Log.d(TAG, "Read fmt chunkSize:" + header.mSubChunk1Size);

        dis.read(shortValue);
        header.mAudioFormat = byteArrayToShort(shortValue);
        Log.d(TAG, "Read audioFormat:" + header.mAudioFormat);

        dis.read(shortValue);
        header.mNumChannel = byteArrayToShort(shortValue);
        Log.d(TAG, "Read channel number:" + header.mNumChannel);

        dis.read(intValue);
        header.mSampleRate = byteArrayToInt(intValue);
        Log.d(TAG, "Read samplerate:" + header.mSampleRate);

        dis.read(intValue);
        header.mByteRate = byteArrayToInt(intValue);
        Log.d(TAG, "Read byterate:" + header.mByteRate);

        dis.read(shortValue);
        header.mBlockAlign = byteArrayToShort(shortValue);
        Log.d(TAG, "Read blockalign:" + header.mBlockAlign);

        dis.read(shortValue);
        header.mBitsPerSample = byteArrayToShort(shortValue);
        Log.d(TAG, "Read bitspersample:" + header.mBitsPerSample);

        header.mSubChunk2ID = "" + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte() + (char) dis.readByte();
        Log.d(TAG, "Read data chunkID:" + header.mSubChunk2ID);

        dis.read(intValue);
        header.mSubChunk2Size = byteArrayToInt(intValue);
        Log.d(TAG, "Read data chunkSize:" + header.mSubChunk2Size);

        Log.d(TAG, "Read wav file success !");
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }

    mWavFileHeader=header;
    return true;
}


private int byteArrayToInt(byte[] b){
    return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getInt();
}

private short byteArrayToShort(byte[] b){
    return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).getShort();
}
}

测试

public class AudioWavActivity extends UIRootActivity {

private Button btn_audio_record;
private Button btn_audio_record_play;


private AudioCapture audioCapture;
private AudioPlayer audioPlayer;

private WavFileWriter wavFileWriter;
private WavFileReader wavFileReader;
private boolean isReading;

private String path="";


@Override
protected int getLayoutId() {
    return R.layout.activity_media_audio;
}

@Override
protected void initTitle() {
    head_title.setText("wav音频文件的存储和解析");
}

@Override
public void initView() {
    btn_audio_record=findViewById(R.id.btn_audio_record);
    btn_audio_record_play=findViewById(R.id.btn_audio_record_play);
}

@Override
public void initData() {
    path=FileUtil.getAudioDir(this)+"/audioTest.wav";
    audioCapture=new AudioCapture();
    audioPlayer=new AudioPlayer();
    wavFileReader=new WavFileReader();
    wavFileWriter=new WavFileWriter();

    String des = "录音权限被禁止,我们需要打开录音权限";
    String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO};
    baseAt.requestPermissions(des, permissions, 100, new PermissionsResultListener() {
        @Override
        public void onPermissionGranted() {

        }
        @Override
        public void onPermissionDenied() {
            finish();

        }
    });

}

@Override
public void initEvent() {
    btn_audio_record.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(event.getAction()==MotionEvent.ACTION_DOWN){
                Log.d("TAG","按住");
                start();
            }else if(event.getAction()==MotionEvent.ACTION_UP){
                Log.d("TAG","松开");
                stop();
            }
            return false;
        }
    });

    btn_audio_record_play.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            play();

        }
    });

}

//播放录音
private void play(){
    isReading=true;
    wavFileReader.openFile(path);
    audioPlayer.startPlay();
    new AudioTrackThread().start();
}

private class AudioTrackThread extends Thread{
    @Override
    public void run() {
        byte[] buffer = new byte[1024];
        while (isReading && wavFileReader.readData(buffer,0,buffer.length)>0){
            audioPlayer.play(buffer,0,buffer.length);
        }
        audioPlayer.stopPlay();
        wavFileReader.closeFile();
    }
}


//开始录音
private void start(){
    wavFileWriter.openFile(path,44100,2,16);
    btn_audio_record.setText("松开 结束");
    audioCapture.startRecord();
    audioCapture.setOnAudioFrameCaptureListener(new AudioCapture.onAudioFrameCaptureListener() {
        @Override
        public void onAudioFrameCapture(byte[] audioData) {
            wavFileWriter.writeData(audioData,0,audioData.length);
        }
    });
}

//结束录音
private void stop(){
    btn_audio_record.setText("按住 录音");
    audioCapture.stopRecord();
    wavFileWriter.closeFile();
}
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容