android音频裁剪(2)——Wav裁剪

=====原创不易,请尊重每一位原创,让我们更有分享的动力,转载请注明=====

转载链接

android音频裁剪(1)——MP3裁剪一文中我分享了对mp3文件裁剪方法。在本文中我将分享对另外一种音频格式——wav格式音频的裁剪。不同于mp3格式的裁剪,对于wav裁剪并不是通过android提供的多媒体库对音频进行处理,而是直接通过java代码对wav音频进行裁剪,所以以下方法对于wav音频裁剪具有平台通用性。
俗话说的好,知己知彼,百战不殆,要对wav文件进行处理,首先要去详细了解wav的文件结构。关于wav文件结构的网上资源很多,但是很杂乱,这里推荐一个地址,http://www.topherlee.com/software/pcm-tut-wavformat.html,个人感觉这里说的算是最简单明了的了。下图是一个该资料的一个关键部分。
What is the header?
The header is the beginning of a WAV (RIFF) file. The header is used to provide specifications on the file type, sample rate, sample size and bit size of the file, as well as its overall length.

The header of a WAV (RIFF) file is 44 bytes long and has the following format:

Positions Sample Value Description
1 - 4 “RIFF” Marks the file as a riff file. Characters are each 1 byte long.
5 - 8 File size (integer) Size of the overall file - 8 bytes, in bytes (32-bit integer). Typically, you’d fill this in after creation.
9 -12 “WAVE” File Type Header. For our purposes, it always equals “WAVE”.
13-16 “fmt “ Format chunk marker. Includes trailing null
17-20 16 Length of format data as listed above
21-22 1 Type of format (1 is PCM) - 2 byte integer
23-24 2 Number of Channels - 2 byte integer
25-28 44100 Sample Rate - 32 byte integer. Common values are 44100 (CD), 48000 (DAT). Sample Rate = Number of Samples per second, or Hertz.
29-32 176400 (Sample Rate * BitsPerSample * Channels) / 8.
33-34 4 (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16 bit mono4 - 16 bit stereo
35-36 16 Bits per sample
37-40 “data” “data” chunk header. Marks the beginning of the data section.
41-44 File size (data) Size of the data section.

表格说得很清楚,如果还有疑惑,也可以看看这位朋友的分享http://blog.csdn.net/bluesoal/article/details/932395。当了解了wav文件的结构之后,再去处理wav文件就变得轻而易举。类似bmp图片的处理,bmp图片的骨骼精髓就在于它的header,wav文件也是一样,当获取了Wav的文件头,就基本知道了整个wav文件架构。所以希望各位朋友先了解wav结构之后再往下看代码。

既然说到裁剪,肯定是有一个新的裁剪得到的文件,按照正常的产品需求来说,裁剪后的音频文件除了时间长度和大小之外其他信息都应该和源音频保持一致。所以生成裁剪文件的时候肯定需要先读取原始音频的header.所以首先要定义一个header类。

public class WavHeader {

    //RITF标志
    public String mRitfWaveChunkID;
    //wav文件大小(总大小-8)
    public int mRitfWaveChunkSize;
    //wav格式
    public String mWaveFormat;

    //格式数据块ID:值为"fmt "(注意后面有个空格)
    public String mFmtChunk1ID;
    //格式数据块大小,一般为16
    public int mFmtChunkSize;
    //数据格式,一般为1,表示音频是pcm编码
    public short mAudioFormat;
    //声道数
    public short mNumChannel;
    //采样率
    public int mSampleRate;
    //每秒字节数
    public int mByteRate;
    //数据块对齐单位
    public short mBlockAlign;
    //采样位数
    public short mBitsPerSample;

    //data块,音频的真正数据块
    public String mDataChunkID;
    //音频实际数据大小
    public int mDataChunkSize;
}

上面的数据结构是按照之前表格定义的,因为wav中的FACT数据块不是必须的,所以在数据结构中并没有关于她们的定义,但对于一半的wav音频,上面header的数据结构已经是完备。下面是一个完整的裁剪流程:
1,创建文件流,读取原音频header
2,创建裁剪保存文件,并根据上面读取的header写入新的音频header(其实就是header 拷贝过去)
3,文件流移至裁剪起点,开始读取数据,将读取数据写入裁剪文件,直到到达裁剪终点,期间需要纪录读取的数据大小
4,将3中纪录的数据大小写入裁剪文件的Header,关闭文件

下面对每一个步骤进行详解。

1,创建文件流,读取原音频header

    //创建原音频文件的文件流
    mDataInputStream = new DataInputStream(new FileInputStream(filepath));

    //读取header
    private boolean readHeader() {

        if (mDataInputStream == null) {
            return false;
        }

        WavHeader header = new WavHeader();

        byte[] buffer = new byte[4];
        byte[] shortBuffer = new byte[2];

        try {
            mDataInputStream.read(buffer);
            header.mRitfWaveChunkID = new String(buffer);
            Log.d(TAG, "Read file chunkID:" + header.mRitfWaveChunkID);

            mDataInputStream.read(buffer);
            header.mRitfWaveChunkSize = byteArrayToInt(buffer);
            Log.d(TAG, "Read file chunkSize:" + header.mRitfWaveChunkSize);

            mDataInputStream.read(buffer);
            header.mWaveFormat = new String(buffer);
            Log.d(TAG, "Read file format:" + header.mWaveFormat);

            mDataInputStream.read(buffer);
            header.mFmtChunk1ID = new String(buffer);
            Log.d(TAG, "Read fmt chunkID:" + header.mFmtChunk1ID);

            mDataInputStream.read(buffer);
            header.mFmtChunkSize = byteArrayToInt(buffer);
            Log.d(TAG, "Read fmt chunkSize:" + header.mFmtChunkSize);

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

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

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

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

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

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

            mDataInputStream.read(buffer);
            header.mDataChunkID = new String(buffer);
            Log.d(TAG, "Read data chunkID:" + header.mDataChunkID);

            mDataInputStream.read(buffer);
            header.mDataChunkSize = byteArrayToInt(buffer);
            Log.d(TAG, "Read data chunkSize:" + header.mDataChunkSize);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        mWavHeader = header;

        return true;
    }

看代码的log,就知道每个读取的数据的含义,我就不重复啰嗦一遍了。下面是我从网上下载的一首wav的歌曲,读取到的文件头:

这里写图片描述

2,创建裁剪保存文件,并根据上面读取的header写入新的音频header(其实就是header 拷贝过去)

//创建裁剪文件
mDataOutputStream = new DataOutputStream(new FileOutputStream(filepath));

//写入header
public boolean writeHeader(WavHeader header) throws IOException {
    if (mDataOutputStream == null) {
        return false;
    }
    if(header == null){
        return false;
    }
    mDataOutputStream.writeBytes(header.mRitfWaveChunkID);
    mDataOutputStream.write(intToByteArray((int) header.mRitfWaveChunkSize), 0, 4);
    mDataOutputStream.writeBytes(header.mWaveFormat);
    mDataOutputStream.writeBytes(header.mFmtChunk1ID);
    mDataOutputStream.write(intToByteArray((int) header.mFmtChunkSize), 0, 4);
    mDataOutputStream.write(shortToByteArray((short) header.mAudioFormat), 0, 2);
    mDataOutputStream.write(shortToByteArray((short) header.mNumChannel), 0, 2);
    mDataOutputStream.write(intToByteArray((int) header.mSampleRate), 0, 4);
    mDataOutputStream.write(intToByteArray((int) header.mByteRate), 0, 4);
    mDataOutputStream.write(shortToByteArray((short) header.mBlockAlign), 0, 2);
    mDataOutputStream.write(shortToByteArray((short) header.mBitsPerSample), 0, 2);
    mDataOutputStream.writeBytes(header.mDataChunkID);
    mDataOutputStream.write(intToByteArray((int) header.mDataChunkSize), 0, 4);
    return true;
}

写入header的过程其实就是读取header的反过程。

3,文件流移至裁剪起点,开始读取数据,将读取数据写入裁剪文件,直到到达裁剪终点,期间需要纪录读取的数据大小

    //移动流到起始点
    public void skip(int time) throws IOException {
        int skip = caculateSkip(time);
        if(mDataInputStream == null){
            return;
        }
        if(skip >= mDataInputStream.available()){
            return;
        }
        mDataInputStream.skipBytes(skip);
    }

    //给定一个时间,计算这个时间对应的文件字节
    private int caculateSkip(int time) {
        double duration = getDuation();
        return (int) ((time * 1.0 / duration) * mWavHeader.mDataChunkSize);
    }

    //获取音频的时长
    private double getDuation() {
        return mWavHeader.mDataChunkSize * 1.0 / mWavHeader.mSampleRate / 4;
    }

   //计算这个时间段对应的音频数据字节数
    public int getIntervalSize(int interval) {
        double duration = getDuation();
        double rate = interval / duration;

        return (int) (mWavHeader.mDataChunkSize * rate);
    }

    //写入数据到裁剪文件
    public boolean writeData(byte[] buffer, int offset, int count) {
        if (mDataOutputStream == null) {
            return false;
        }
        try {
            mDataOutputStream.write(buffer, offset, count);
            mDataSize += count;//纪录写入裁剪文件的实际数据大小
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

这个步骤涉及到上面几个关键函数,首先原始音频文件文件流需要通过skip()函数移动到对应的裁剪起点,裁剪终点减去裁剪起点得到裁剪时间段,通过getIntervalSize()函数就能计算出这个时间段对应的音频数据的字节数量,在读取的过程中通过writeData()函数把数据写入的裁剪文件中,写入过程中需要纪录写入的数据大小,当读取的字节数量超过裁剪时间段对应的字节数量,说明裁剪结束。

4,将3中纪录的数据大小写入裁剪文件的Header,关闭文件

回头看看wav header的定义

 public class WavHeader {

    //RITF标志
    public String mRitfWaveChunkID;
    //wav文件大小(总大小-8)
    public int mRitfWaveChunkSize;
    //wav格式
    public String mWaveFormat;

    //格式数据块ID:值为"fmt "(注意后面有个空格)
    public String mFmtChunk1ID;
    //格式数据块大小,一般为16
    public int mFmtChunkSize;
    //数据格式,一般为1,表示音频是pcm编码
    public short mAudioFormat;
    //声道数
    public short mNumChannel;
    //采样率
    public int mSampleRate;
    //每秒字节数
    public int mByteRate;
    //数据块对齐单位
    public short mBlockAlign;
    //采样位数
    public short mBitsPerSample;

    //data块,音频的真正数据块
    public String mDataChunkID;
    //音频实际数据大小
    public int mDataChunkSize;
}

裁剪出来的文件和原始音频文件有两个地方是不同的,一个是mRitfWaveChunkSize(wav文件大小),一个是mDataChunkSize(音频实际数据大小),所以这两个地方在裁剪文件中修改,根据在写入的时候纪录的数据大小(mDataSize)就能得到这些数据,通过RandomAccessFile把它们写入:

    private boolean writeDataSize() {
        if (mDataOutputStream == null) {
            return false;
        }
        try {
            RandomAccessFile wavFile = new RandomAccessFile(mFilepath, "rw");
            //偏移mRitfWaveChunkID 占用的4字节
            wavFile.seek(4);
            //写入mRitfWaveChunkSize(wav文件大小)
            wavFile.write(intToByteArray((int) (mDataSize + 44 - 8)), 0, 4);
            //偏移40字节,到mDataChunkSize对应的字节位置
            wavFile.seek(40);
            //写入裁剪音频的实际数据大小
            wavFile.write(intToByteArray((int) (mDataSize)), 0, 4);
            wavFile.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    //裁剪文件关闭的时候写入实际音频数据大小
    public boolean closeFile() throws IOException {
        boolean ret = true;
        if (mDataOutputStream != null) {
            ret = writeDataSize();
            mDataOutputStream.close();
            mDataOutputStream = null;
        }
        return ret;
    }

总结,相信以上步骤能很轻易明白wav音频的裁剪原理,下面是一个综合过程:

public class WavHelper {
    public static boolean clip(String src, String dst, int start, int end){
        try {
            //创建原始音频文件流
            WavReader reader = new WavReader();
            reader.openFile(src);
            //读取header
            WavHeader header = reader.getmWavFileHeader();
            //创建裁剪文件输出文件流
            WavWriter writer = new WavWriter();
            writer.openFile(dst, header);
            //BYTE_PER_READ 指的是每次读取的字节数,可以自定义
            byte[] buffer = new byte[WavReader.BYTE_PER_READ];
            int size = -1;
            //移动至裁剪起点
            reader.skip(start);
            //获取裁剪时间段对应的字节大小
            int dataSize = reader.getIntervalSize(end - start);
            int sizeCount = 0;
            while(true){
                size = reader.readData(buffer, 0, buffer.length);
                //当到达裁剪时间段大小时候结束读取
                if(size < 0 || sizeCount >= dataSize){
                    //在close时候写入实际音频数据大小
                    writer.closeFile();
                    reader.closeFile();
                    return true;
                }
                //写入音频数据到裁剪文件
                writer.writeData(buffer, 0, size);
                //计算读取的字节数,注意,因为BYTE_PER_READ的原因,读取的字节数和实际的音频大小未必相同,
                //不能把它直接当作实际音频数据大小
                sizeCount += size;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
}

最后,通过以上步骤可以很轻易就完成wav音频的裁剪过程,希望能帮到有需要的朋友。有什么不明白的地方,欢迎私信或者发表评论,大家一起研究,共同进步。如果需要裁剪mp3音频的朋友,可以参考我上一篇文章:android音频裁剪(1)——MP3裁剪

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试

关闭