WAV文件格式解析及處理

RIFF file format

RIFF全稱為資源互換文件格式(Resources Interchange File Format),是Windows下大部分多媒體文件遵循的一種文件結(jié)構(gòu)掰伸。RIFF文件所包含的數(shù)據(jù)類型由該文件的擴(kuò)展名來(lái)標(biāo)識(shí)

Chunk

RIFF文件結(jié)構(gòu)可以看作是樹(shù)狀結(jié)構(gòu)技扼,其基本構(gòu)成是稱為"塊"(Chunk)的單元姥芥,每個(gè)塊有"標(biāo)志符"耗啦、"數(shù)據(jù)大小"及"數(shù)據(jù)"所組成

 public static class Chunk {
        //4個(gè)字節(jié)
        public String chunkId;
        //4個(gè)字節(jié)。指的是 data的長(zhǎng)度
        public int dataSize;
        public byte[] data;
    }
  • chunkId
    4字節(jié)胳挎,用以標(biāo)識(shí)塊中所包含的數(shù)據(jù)。如:RIFF,LIST,fmt,data,WAV,AVI等掸读。RIFF文件是按照小端 little-endian字節(jié)順序?qū)懭氲摹?/li>
  • dataSize
    存儲(chǔ)在data域中的數(shù)據(jù)長(zhǎng)度
  • data
    包含數(shù)據(jù)串远,數(shù)據(jù)以字為單位存放宏多,如果數(shù)據(jù)長(zhǎng)度為奇數(shù)(字節(jié)為單位),則最后添加一個(gè)空字節(jié)澡罚。

chunk是可以嵌套的伸但,但是只有塊標(biāo)志為RIFF或者LISTchunk才能包含其他的chunk

RIFF chunk

標(biāo)志為RIFFchunk是比較特殊的留搔,每一個(gè)RIFF文件首先存放的必須是一個(gè)RIFF chunk更胖,并且只能有這一個(gè)標(biāo)志為RIFFchunkRIFF的數(shù)據(jù)域的起始位置是一個(gè)4字節(jié)碼(FOURCC)隔显,用于標(biāo)識(shí)其數(shù)據(jù)域中chunk的數(shù)據(jù)類型却妨;緊接著數(shù)據(jù)域的內(nèi)容則是包含的subchunk,如下圖

RIFF chunk

這是一個(gè)RIFF chunk中包含有兩個(gè)subchunk括眠,可以看出RIFF chunk的數(shù)據(jù)域首先是是4字節(jié)的 Form Type彪标,接著是兩個(gè)subchunk,每一個(gè)subchun有包含有自己的標(biāo)識(shí)掷豺、數(shù)據(jù)域的大小以及數(shù)據(jù)域捞烟。
除了RIFF cunk可以嵌套其他的chunk外,另一個(gè)可以有subchunk的就是LIST chunk当船。

image

上圖中题画,首先是RIFF文件必須的RIFF chunk,其數(shù)據(jù)域又包含有兩個(gè)subchunk德频,其中一個(gè)subchunk的類型為LIST苍息,該LIST chunk又包含了兩個(gè)subchunk。

FourCC

FourCC 全稱為Four-Character Codes壹置,是一個(gè)4字節(jié)32位的標(biāo)識(shí)符竞思,通常用來(lái)標(biāo)識(shí)文件的數(shù)據(jù)格式。例如钞护,在音視頻播放器中衙四,可以通過(guò) 文件的FourCC來(lái)決定調(diào)用那種CODEC進(jìn)行視音頻的解碼。例如:DIV3,DIV4,DIVX,H264等患亿,對(duì)于音頻則有:WAV,MP3等。對(duì)于上面的RIFF文件押逼,則有:RIFF,WAVE,fmt,data等步藕。FourCC是4個(gè)ASCII字符,不足四個(gè)字符的則在最后補(bǔ)充空格(不是空字符)挑格。比如咙冗,F(xiàn)ourCC fmt,實(shí)際上是'f' 'm' 't' ' '漂彤。


WAV

WAV 是Microsoft開(kāi)發(fā)的一種音頻文件格式雾消,它符合上面提到的RIFF文件格式標(biāo)準(zhǔn)灾搏,可以看作是RIFF文件的一個(gè)具體實(shí)例。既然WAV符合RIFF規(guī)范立润,其基本的組成單元也是chunk狂窑。一個(gè)WAV文件通常有三個(gè)chunk以及一個(gè)可選chunk,其在文件中的排列方式依次是:RIFF chunk桑腮,F(xiàn)ormat chunk泉哈,F(xiàn)act chunk(附加塊,可選)破讨,Data chunk丛晦。

image.png

一個(gè)WAV文件,首先是一個(gè)RIFF chunk提陶;RIFF chunk又包含有Format chunk烫沙,Data chunk以及可選的Fact chunk。各個(gè)chunk中字段的意義如下:

RIFF chunk

id size data
'R' 'I' 'F' 'F' 其data字段中數(shù)據(jù)的大小 字節(jié)數(shù) 包含其他的chunk

Format chunk

id size data
'f' 'm' 't' ' ' 見(jiàn)下面Chunk Size 見(jiàn)下面Chunk Data
chunk size

數(shù)據(jù)字段包含數(shù)據(jù)的大小隙笆。如無(wú)擴(kuò)展塊锌蓄,則值為16;有擴(kuò)展塊仲器,則值為= 16 + 2字節(jié)擴(kuò)展塊長(zhǎng)度 + 擴(kuò)展塊長(zhǎng)度或者值為18(只有擴(kuò)展塊的長(zhǎng)度為2字節(jié)煤率,值為0)

chunk Data

存放音頻格式、聲道數(shù)乏冀、采樣率等信息

  • format_tag
    2字節(jié)蝶糯,表示音頻數(shù)據(jù)的格式。如值為1辆沦,表示使用PCM格式昼捍。
  • channels
    2字節(jié),聲道數(shù)肢扯。值為1則為單聲道妒茬,為2則是雙聲道。
  • samples_per_sec
    采樣率蔚晨,主要有22.05KHz乍钻,44.1kHz和48KHz。
  • bytes_per sec
    音頻的碼率铭腕,每秒播放的字節(jié)數(shù)银择。samples_per_sec * channels * bits_per_sample / 8,可以估算出使用緩沖區(qū)的大小
  • block_align
    數(shù)據(jù)塊對(duì)齊單位累舷,一次采樣的大小浩考,值為聲道數(shù) * 量化位數(shù) / 8,在播放時(shí)需要一次處理多個(gè)該值大小的字節(jié)數(shù)據(jù)被盈。
  • bits_per_sample
    音頻sample的量化位數(shù)析孽,有16位搭伤,24位和32位等。
  • cbSize
    擴(kuò)展區(qū)的長(zhǎng)度
擴(kuò)展塊內(nèi)容

22字節(jié)袜瞬,具體介紹怜俐,后面補(bǔ)充。

  • Fact chunk**(option)
id size 采樣總數(shù)
'f' 'a' 'c' 't' 數(shù)據(jù)域的長(zhǎng)度吞滞,4(最小值為4) 采樣總數(shù) (每個(gè)聲道)

采用壓縮編碼的WAV文件佑菩,必須要有Fact chunk,該塊中只有一個(gè)數(shù)據(jù)裁赠,為每個(gè)聲道的采樣總數(shù)殿漠。

Data chunk

id size data
'd' 'a' 't' 'a' 數(shù)據(jù)域的長(zhǎng)度 具體的音頻數(shù)據(jù)存放在這里

補(bǔ)充

Format chunk 中的編碼方式

在Format chunk中,除了有音頻的數(shù)據(jù)的采樣率佩捞、聲道等音頻的屬性外绞幌,另一個(gè)比較主要的字段就是format_tag,該字段表示音頻數(shù)據(jù)是以何種方式編碼存放的一忱。其具體的取值可以為以下:

格式代碼 格式名稱 fmt 塊長(zhǎng)度 fact 塊
1(0x0001) PCM/非壓縮格式 16
2(0x0002 Microsoft ADPCM 18
3(0x0003) IEEE float 18
6(0x0006) ITU G.711 a-law 18
7(0x0007) ITU G.711 μ-law 18
49(0x0031) GSM 6.10 20
64(0x0040) ITU G.721 ADPCM
65,534(0xFFFE) 見(jiàn)子格式塊中的編碼格式 40
關(guān)于擴(kuò)展格式塊

當(dāng)WAV文件使用的不是PCM編碼方式是莲蜘,就需要擴(kuò)展格式塊,它是在基本的Format chunk又添加一段數(shù)據(jù)帘营。該數(shù)據(jù)的前兩個(gè)字節(jié)票渠,表示的擴(kuò)展塊的長(zhǎng)度。緊接其后的是擴(kuò)展的數(shù)據(jù)區(qū)芬迄,含有擴(kuò)展的格式信息问顷,其具體的長(zhǎng)度取決于壓縮編碼的類型。當(dāng)某種編碼方式(如 ITU G.711 a-law)使擴(kuò)展區(qū)的長(zhǎng)度為0禀梳,擴(kuò)展區(qū)的長(zhǎng)度字段還必須保留杜窄,只是其值設(shè)置為0。
擴(kuò)展區(qū)的各個(gè)字節(jié)的含義如下:

  • size 2字節(jié)
    擴(kuò)展區(qū)的數(shù)據(jù)長(zhǎng)度 算途,可以為0或22
  • valid_bits_per_sample 2字節(jié)
    有效的采樣位數(shù)塞耕,最大值為采樣字節(jié)數(shù) * 8∽烊浚可以使用更靈活的量化位數(shù)扫外,通常音頻sample的量化位數(shù)為8的倍數(shù),但是使用了WAVE_FORMAT_EXTENSIBLE時(shí)廓脆,量化的位數(shù)有擴(kuò)展區(qū)中的valid bits per sample來(lái)描述畏浆,可以小于Format chunk中制定的bits per sample
  • channle mask 4字節(jié)
    聲道掩碼
  • sub format 16字節(jié)
    GUID狞贱,include the data format code,數(shù)據(jù)格式碼蜀涨。

在Format chunk中的format_tag設(shè)置為0xFFFE時(shí)瞎嬉,表示使用擴(kuò)展區(qū)中的sub_format來(lái)決定音頻的數(shù)據(jù)的編碼方式蝎毡。在以下幾種情況下必須要使用WAVE_FORMAT_EXTENSIBLE

  • PCM數(shù)據(jù)的量化位數(shù)大于16
  • 音頻的采樣聲道大于2
  • 實(shí)際的量化位數(shù)不是8的倍數(shù)
  • 存儲(chǔ)順序和播放順序不一致,需要指定從聲道順序到聲卡播放順序的映射情況氧枣。

Data chunk

Data塊中存放的是音頻的采樣數(shù)據(jù)沐兵。每個(gè)sample按照采樣的時(shí)間順序?qū)懭耄瑢?duì)于使用多個(gè)字節(jié)的sample便监,使用小端模式存放(低位字節(jié)存放在低地址扎谎,高位字節(jié)存放在高地址)。對(duì)于多聲道的sample采用交叉存放的方式烧董。例如:立體雙聲道的sample存儲(chǔ)順序?yàn)椋郝暤?的第一個(gè)sample毁靶,聲道2的第一個(gè)sample;聲道1的第二個(gè)sample逊移,聲道2的第二個(gè)sample预吆;依次類推....。對(duì)于PCM數(shù)據(jù)胳泉,有以下兩種的存儲(chǔ)方式:

  • 單聲道拐叉,量化位數(shù)為8,使用偏移二進(jìn)制碼
  • 除上面之外的扇商,使用補(bǔ)碼方式存儲(chǔ)凤瘦。

實(shí)例分析

普通的WAV

image.png
RIFF塊

由上面的介紹可知,由RIFF格式固定的案铺。包括RIFF蔬芥、SizeFOURCC

  • RIFF


    RIFF.png
  • Size


    Size.png

    因?yàn)槭切《说捻樞颉?shí)際上的十六進(jìn)制數(shù)應(yīng)該是 “00077090”红且,轉(zhuǎn)為487568坝茎。這個(gè)數(shù)值+8,就是文件的長(zhǎng)度暇番。

  • WAVE


    WAVE.png
Format chunk
  • ChunkId
    "fmt "嗤放。和上面標(biāo)識(shí)的一樣。是4個(gè)字節(jié)壁酬,不足補(bǔ)“ ”


    image.png
  • Chunk Size


    image.png

    因?yàn)槭切《说捻樞虼巫谩?shí)際上的十六進(jìn)制數(shù)應(yīng)該是 “00000010”,為16舆乔。就是后續(xù)的Data的長(zhǎng)度岳服。

  • Chunk Data
    fmt chunk中的chunk data就是包含有該視頻的信息。

    Chunk Data.png

名稱 偏移地址 字節(jié)數(shù) 端序 內(nèi)容 當(dāng)前值
AudioFormat 0x08 2Byte 小端 音頻格式 1希俩,PCM音頻數(shù)據(jù)的值為1吊宋。則當(dāng)前沒(méi)有Fact chunk
NumChannels 0x0A 2Byte 小端 聲道數(shù) 2,表示音頻數(shù)據(jù)的聲道數(shù),1:?jiǎn)温暤溃?:雙聲道颜武。
SampleRate 0x0C 4Byte 小端 采樣率 44100
ByteRate 0x10 4Byte 小端 每秒數(shù)據(jù)字節(jié)數(shù) 176400璃搜。SampleRate * NumChannels * BitsPerSample / 8
BlockAlign 0x14 2Byte 小端 數(shù)據(jù)塊對(duì)齊 4拖吼。NumChannels * BitsPerSample / 8
BitsPerSample 0x16 2Byte 小端 采樣位數(shù) 采樣深度16bit。8:8bit这吻,16:16bit吊档,32:32bit
Data

因?yàn)槭荘CM的數(shù)據(jù)格式,所以直接就到了data

  • 標(biāo)識(shí)'data'


    data.png
  • 音頻數(shù)據(jù)的長(zhǎng)度Size
    Size.png
名稱 偏移地址 字節(jié)數(shù) 端序 內(nèi)容 當(dāng)前值
ID 0x00 4Byte 大端 'data' (0x64617461) “0x77000”唾糯,轉(zhuǎn)為十進(jìn)制為 487424 怠硼。
Size 0x04 4Byte 小端 N 等于 ByteRate * seconds ,約為2.7秒移怯。
Data 0x08 NByte 小端 音頻數(shù)據(jù) ...
總結(jié)
  • 頭部大小
    通常的WAV香璃,以PCM為數(shù)據(jù)格式的,基本上頭部就如上面的結(jié)構(gòu)芋酌。頭部的SIZE為固定的44,
    通常對(duì)WAV音頻進(jìn)行處理時(shí)增显,會(huì)直接寫(xiě)死這個(gè)頭部的Offset

排查一次WAV處理中的雜音情況

但是在實(shí)際處理的過(guò)程中脐帝,遇到了下面這樣的WAV HEADER同云。頭部的長(zhǎng)度不同,導(dǎo)致后續(xù)的音頻處理中出現(xiàn)了雜音的情況堵腹。排查之后秒梳,才發(fā)現(xiàn)是因?yàn)轭^部大小不同導(dǎo)致彭雾。

特殊一點(diǎn)的WAV

由Adobe Premiere Pro CC 創(chuàng)建的WAV届巩。

image.png

它包含有LIST Chunk峦朗。而且fmt chunk的size為18。
wav list.png

因?yàn)橛蠰IST,導(dǎo)致上面通常寫(xiě)死的HEAD_SIZE 44出現(xiàn)錯(cuò)誤腿堤。
這個(gè)時(shí)候重新去計(jì)算這個(gè)HEAD_SIZE就可以了阀坏。

LIST CHUNK
  • CHUNK ID
    CHUNK ID為“LIST”
  • CHUNK SIZE
    可以看到為0x58,十進(jìn)制為88笆檀。
計(jì)算HEAD_SIZE
    private static int getHeadSize(RandomAccessFile srcFis) throws IOException {
        int offset = 0;
        //riff
        getChunkId(srcFis);
        offset += 4;
        //length
        getChunkSize(srcFis);
        offset += 4;
        //wave
        getChunkId(srcFis);
        offset += 4;
        //fmt
        getChunkId(srcFis);
        offset += 4;
        //fmt length
        int skipLength = getChunkSize(srcFis);
        offset += 4;
        byte[] skipBytes = new byte[skipLength];
        srcFis.read(skipBytes);
        offset += skipLength;
        String chunkId = getChunkId(srcFis);
        offset += 4;
        while (!chunkId.equals("data")) {
            skipLength = getChunkSize(srcFis);
            offset += 4;
            skipBytes = new byte[skipLength];
            srcFis.read(skipBytes);
            offset += skipLength;
            chunkId = getChunkId(srcFis);
            offset += 4;
        }
        offset += 4;
        System.out.println("headSize="+offset);
        return offset;
    }

    private static int getChunkSize(RandomAccessFile srcFis) throws IOException {
        byte[] formatSize = new byte[4];
        srcFis.read(formatSize);
        int fisrt8 = formatSize[0] & 0xFF;  
        int fisrt16 = formatSize[1] & 0xFF;
        int fisrt24 = formatSize[2] & 0xFF;
        int fisrt32 = formatSize[3] & 0xFF;
        int chunkSize = fisrt8 | (fisrt16 << 8) | (fisrt24 << 16) | (fisrt32 << 24);
        System.out.println("ChunkSize=" + chunkSize);
        return chunkSize;
    }

    private static String getChunkId(RandomAccessFile srcFis) throws IOException {
        byte[] bytes = new byte[4];
        srcFis.read(bytes);
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            stringBuilder.append((char) bytes[i]);
        }
        String chunkId = stringBuilder.toString();
        System.out.println("ChunkId=" + chunkId);
        return chunkId;
    }

只有這樣計(jì)算出的HEAD_SIZE才能正確的處理文件忌堂,避免因?yàn)檫@個(gè)原因?qū)е碌碾s音。


WAV一些處理

獲取wave文件某個(gè)時(shí)間對(duì)應(yīng)的數(shù)據(jù)位置

    private static int getPositionFromWave(float time, int sampleRate, int channels, int bitNum) {
        int byteNum = bitNum / 8;
        //時(shí)間* 每秒數(shù)據(jù)字節(jié)數(shù)= 當(dāng)前時(shí)間的字節(jié)數(shù)
        int position = (int) (time * sampleRate * channels * byteNum);
        //當(dāng)前時(shí)間的字節(jié)數(shù) / 每個(gè)采樣所需的字節(jié)數(shù) * 當(dāng)前時(shí)間的字節(jié)數(shù) 來(lái)進(jìn)行取整酗洒。定位到一個(gè)完整的采樣的起點(diǎn)
        position = position / (byteNum * channels) * (byteNum * channels);

        return position;
    }
  • 當(dāng)前時(shí)間的字節(jié)數(shù)
    sampleRate * channels * byteNum
  • 定位到完整的采樣時(shí)間的起點(diǎn)
    position = position / (byteNum * channels) * (byteNum * channels);

剪切音頻

剪切音頻的流程很簡(jiǎn)單

  1. 計(jì)算兩個(gè)采樣點(diǎn)的位置士修。偏移頭部的大小,復(fù)制兩個(gè)采樣點(diǎn)之間的數(shù)據(jù)樱衷。
  2. 重新寫(xiě)入修改之后的頭部棋嘲。因?yàn)閿?shù)據(jù)長(zhǎng)度修改。里面的RIFFChunkSizedata塊的長(zhǎng)度由當(dāng)前的長(zhǎng)度做對(duì)應(yīng)修改矩桂。
 public static void cutAudio(Audio audio, Audio audioOut, float cutStartTime, float cutEndTime) {
        if (cutStartTime == 0 && cutEndTime == audio.getTimeMillis() / 1000f) {
            return;
        }
        if (cutStartTime >= cutEndTime) {
            return;
        }

        String srcWavePath = audio.getPath();
        int sampleRate = audio.getSampleRate();
        int channels = audio.getChannel();
        int bitNum = audio.getBitNum();
        RandomAccessFile srcFis = null;
        RandomAccessFile newFos = null;
        String tempOutPath = srcWavePath + ".temp";
        try {

            //創(chuàng)建輸入流
            srcFis = new RandomAccessFile(srcWavePath, "rw");
            newFos = new RandomAccessFile(tempOutPath, "rw");

            //源文件開(kāi)始讀取位置沸移,結(jié)束讀取文件,讀取數(shù)據(jù)的大小
            final int cutStartPos = getPositionFromWave(cutStartTime, sampleRate, channels, bitNum);
            final int cutEndPos = getPositionFromWave(cutEndTime, sampleRate, channels, bitNum);
            final int contentSize = cutEndPos - cutStartPos;

            //復(fù)制wav head 字節(jié)數(shù)據(jù)
            byte[] headerData = AudioEncodeUtil.getWaveHeader(contentSize, sampleRate, channels, bitNum);
            copyHeadData(headerData, newFos);

            //取到正確頭部偏移
            int srcHeadSize = getHeadSize(srcFis);
            //移動(dòng)到文件開(kāi)始讀取處
            srcFis.seek(srcHeadSize + cutStartPos);

            //復(fù)制裁剪的音頻數(shù)據(jù)
            copyData(srcFis, newFos, contentSize);

        } catch (Exception e) {
            e.printStackTrace();

            return;

        } finally {
            //關(guān)閉輸入流
            if (srcFis != null) {
                try {
                    srcFis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (newFos != null) {
                try {
                    newFos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //重命名為源文件
        FileUtil.renameFile(new File(tempOutPath), audioOut.getPath());

    }

    public static byte[] getWaveHeader(long totalAudioLen, int sampleRate, int channels, int bitNum) throws IOException {

        //總大小,由于不包括RIFF和WAV阔籽,所以是44 - 8 = 36流妻,在加上PCM文件大小
        long totalDataLen = totalAudioLen + 36;
        //采樣字節(jié)byte率
        long byteRate = sampleRate * channels * bitNum / 8;

        byte[] header = new byte[44];
        header[0] = 'R'; // RIFF
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);//數(shù)據(jù)大小
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//過(guò)渡字節(jié)
        //數(shù)據(jù)大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //編碼方式 10H為PCM編碼格式
        header[20] = 1; // format = 1
        header[21] = 0;
        //通道數(shù)
        header[22] = (byte) channels;
        header[23] = 0;
        //采樣率,每個(gè)通道的播放速度
        header[24] = (byte) (sampleRate & 0xff);
        header[25] = (byte) ((sampleRate >> 8) & 0xff);
        header[26] = (byte) ((sampleRate >> 16) & 0xff);
        header[27] = (byte) ((sampleRate >> 24) & 0xff);
        //音頻數(shù)據(jù)傳送速率,采樣率*通道數(shù)*采樣深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 確定系統(tǒng)一次要處理多少個(gè)這樣字節(jié)的數(shù)據(jù)笆制,確定緩沖區(qū),通道數(shù)*采樣位數(shù)
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        //每個(gè)樣本的數(shù)據(jù)位數(shù)
        header[34] = 16;
        header[35] = 0;
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

        return header;
    }

替換和插入音頻

  1. 計(jì)算兩個(gè)采樣點(diǎn)的位置涣达。偏移頭部的大小在辆,講兩個(gè)采樣點(diǎn)之間的數(shù)據(jù),替換成想要的音頻度苔。
  2. 重新寫(xiě)入修改之后的頭部匆篓。因?yàn)閿?shù)據(jù)長(zhǎng)度修改。里面的RIFFChunkSizedata塊的長(zhǎng)度由當(dāng)前的長(zhǎng)度做對(duì)應(yīng)修改寇窑。
public static void replaceAudioWithSame(Audio srcAudio, Audio coverAudio, Audio outAudio, float srcStartTime) {

        String srcWavePath = srcAudio.getPath();
        String coverWavePath = coverAudio.getPath();
        int sampleRate = srcAudio.getSampleRate();
        int channels = srcAudio.getChannel();
        int bitNum = srcAudio.getBitNum();
        RandomAccessFile srcFis = null;
        RandomAccessFile coverFis = null;
        RandomAccessFile newFos = null;
        String tempOutPcmPath = srcWavePath + ".tempPcm";
        try {

            //創(chuàng)建輸入流
            srcFis = new RandomAccessFile(srcWavePath, "rw");
            coverFis = new RandomAccessFile(coverWavePath, "rw");
            newFos = new RandomAccessFile(tempOutPcmPath, "rw");

            int srcHeadSize = getHeadSize(srcFis);
            int coverHeadSize = getHeadSize(coverFis);

            final int srcStartPos = getPositionFromWave(srcStartTime, sampleRate, channels, bitNum);
            final int coverStartPos = 0;
            final int coverEndPos = (int) coverFis.length() - coverHeadSize;


            //復(fù)制源音頻srcStartTime時(shí)間之前的數(shù)據(jù)
            //跳過(guò)頭文件數(shù)據(jù)
            srcFis.seek(srcHeadSize);

            copyData(srcFis, newFos, srcStartPos);

            //復(fù)制覆蓋音頻指定時(shí)間段的數(shù)據(jù)
            //跳過(guò)指定位置數(shù)據(jù)
            coverFis.seek(coverHeadSize + coverStartPos);

            int copyCoverSize = coverEndPos - coverStartPos;
            float volume = coverAudio.getVolume();
            copyData(coverFis, newFos, copyCoverSize);

            //復(fù)制srcStartTime時(shí)間后的源文件數(shù)據(jù)
            final int srcStartAddCoverPosition = getPositionFromWave(srcStartTime + ((float) coverAudio.getTimeMillis()) / 1000, sampleRate, channels, bitNum);

            final long srcFileSize = srcFis.length() - srcHeadSize;
            int remainSize = (int) (srcFileSize - srcStartAddCoverPosition);
            if (remainSize > 0) {

//                coverFis.seek(WAVE_HEAD_SIZE + coverStartPos);
                srcFis.seek(srcHeadSize + srcStartAddCoverPosition);
                copyData(srcFis, newFos, remainSize);

            }

        } catch (Exception e) {
            e.printStackTrace();

            return;

        } finally {
            //關(guān)閉輸入流
            if (srcFis != null) {
                try {
                    srcFis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (coverFis != null) {
                try {
                    coverFis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (newFos != null) {
                try {
                    newFos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        // 刪除源文件,
//        new File(srcWavePath).delete();
        // 轉(zhuǎn)換臨時(shí)文件為源文件
        AudioEncodeUtil.convertPcm2Wav(tempOutPcmPath, outAudio.getPath(), sampleRate, channels, bitNum);
        //刪除臨時(shí)文件
        new File(tempOutPcmPath).delete();
    }

參考

RIFF和WAVE音頻文件格式
WAV文件格式詳解
wav文件格式分析與詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸦概,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子甩骏,更是在濱河造成了極大的恐慌窗市,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饮笛,死亡現(xiàn)場(chǎng)離奇詭異咨察,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)福青,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門摄狱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人无午,你說(shuō)我怎么就攤上這事媒役。” “怎么了宪迟?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵酣衷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我踩验,道長(zhǎng)鸥诽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任箕憾,我火速辦了婚禮牡借,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘袭异。我一直安慰自己钠龙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著碴里,像睡著了一般沈矿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咬腋,一...
    開(kāi)封第一講書(shū)人閱讀 49,837評(píng)論 1 290
  • 那天羹膳,我揣著相機(jī)與錄音,去河邊找鬼根竿。 笑死陵像,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的寇壳。 我是一名探鬼主播醒颖,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼壳炎!你這毒婦竟也來(lái)了泞歉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匿辩,失蹤者是張志新(化名)和其女友劉穎腰耙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撒汉,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沟优,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了睬辐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挠阁。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖溯饵,靈堂內(nèi)的尸體忽然破棺而出侵俗,到底是詐尸還是另有隱情,我是刑警寧澤丰刊,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布隘谣,位于F島的核電站,受9級(jí)特大地震影響啄巧,放射性物質(zhì)發(fā)生泄漏寻歧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一秩仆、第九天 我趴在偏房一處隱蔽的房頂上張望码泛。 院中可真熱鬧,春花似錦澄耍、人聲如沸噪珊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)痢站。三九已至磷箕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阵难,已是汗流浹背岳枷。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呜叫,地道東北人嫩舟。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像怀偷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子播玖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容