Android音頻開發(fā)(4):PCM轉(zhuǎn)WAV格式音頻

Android 音頻開發(fā) 目錄

  1. Android音頻開發(fā)(1):音頻相關(guān)知識
  2. Android音頻開發(fā)(2):使用AudioRecord錄制pcm格式音頻
  3. Android音頻開發(fā)(3):使用AudioRecord實現(xiàn)錄音的暫停和恢復(fù)
  4. Android音頻開發(fā)(4):PCM轉(zhuǎn)WAV格式音頻
  5. Android音頻開發(fā)(5):Mp3的錄制 - 編譯Lame源碼
  6. Android音頻開發(fā)(6):Mp3的錄制 - 使用Lame實時錄制MP3格式音頻
  7. Android音頻開發(fā)(7):音樂可視化-FFT頻譜圖

項目地址

https://github.com/zhaolewei/ZlwAudioRecorder


前面幾篇已經(jīng)介紹了PCM音頻文件的錄制,這一篇主要介紹下pcm轉(zhuǎn)wav。

一、wav 和 pcm

一般通過麥克風(fēng)采集的錄音數(shù)據(jù)都是PCM格式的,即不包含頭部信息,播放器無法知道音頻采樣率、位寬等參數(shù),導(dǎo)致無法播放,顯然是非常不方便的。pcm轉(zhuǎn)換成wav,我們只需要在pcm的文件起始位置加上至少44個字節(jié)的WAV頭信息即可。

RIFF

  • WAVE文件是以RIFF(Resource Interchange File Format, "資源交互文件格式")格式來組織內(nèi)部結(jié)構(gòu)的
    RIFF文件結(jié)構(gòu)可以看作是樹狀結(jié)構(gòu),其基本構(gòu)成是稱為"塊"(Chunk)的單元.
  • WAVE文件是由若干個Chunk組成的。按照在文件中的出現(xiàn)位置包括:RIFF WAVE Chunk, Format Chunk, Fact Chunk(可選), Data Chunk。

Fact Chunk 在壓縮后或在非PCM編碼時存在

二、WAV頭文件

所有的WAV都有一個文件頭,這個文件頭記錄著音頻流的編碼參數(shù)。數(shù)據(jù)塊的記錄方式是little-endian字節(jié)順序。

image
偏移地址 命名 內(nèi)容
00-03 ChunkId "RIFF"
04-07 ChunkSize 下個地址開始到文件尾的總字節(jié)數(shù)(此Chunk的數(shù)據(jù)大小)
08-11 fccType "WAVE"
12-15 SubChunkId1 "fmt ",最后一位空格。
16-19 SubChunkSize1 一般為16,表示fmt Chunk的數(shù)據(jù)塊大小為16字節(jié),即20-35
20-21 FormatTag 1:表示是PCM 編碼
22-23 Channels 聲道數(shù),單聲道為1,雙聲道為2
24-27 SamplesPerSec 采樣率
28-31 BytesPerSec 碼率 :采樣率 * 采樣位數(shù) * 聲道個數(shù),bytePerSecond = sampleRate * (bitsPerSample / 8) * channels
32-33 BlockAlign 每次采樣的大?。何粚?聲道數(shù)/8
34-35 BitsPerSample 位寬
36-39 SubChunkId2 "data"
40-43 SubChunkSize2 音頻數(shù)據(jù)的長度
44-... data 音頻數(shù)據(jù)

三、java 生成頭文件

  1. WavHeader.class
    public static class WavHeader {
        /**
         * RIFF數(shù)據(jù)塊
         */
        final String riffChunkId = "RIFF";
        int riffChunkSize;
        final String riffType = "WAVE";
    
        /**
         * FORMAT 數(shù)據(jù)塊
         */
        final String formatChunkId = "fmt ";
        final int formatChunkSize = 16;
        final short audioFormat = 1;
        short channels;
        int sampleRate;
        int byteRate;
        short blockAlign;
        short sampleBits;
    
        /**
         * FORMAT 數(shù)據(jù)塊
         */
        final String dataChunkId = "data";
        int dataChunkSize;
    
        WavHeader(int totalAudioLen, int sampleRate, short channels, short sampleBits) {
            this.riffChunkSize = totalAudioLen;
            this.channels = channels;
            this.sampleRate = sampleRate;
            this.byteRate = sampleRate * sampleBits / 8 * channels;
            this.blockAlign = (short) (channels * sampleBits / 8);
            this.sampleBits = sampleBits;
            this.dataChunkSize = totalAudioLen - 44;
        }
    
        public byte[] getHeader() {
            byte[] result;
            result = ByteUtils.merger(ByteUtils.toBytes(riffChunkId), ByteUtils.toBytes(riffChunkSize));
            result = ByteUtils.merger(result, ByteUtils.toBytes(riffType));
            result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkId));
            result = ByteUtils.merger(result, ByteUtils.toBytes(formatChunkSize));
            result = ByteUtils.merger(result, ByteUtils.toBytes(audioFormat));
            result = ByteUtils.merger(result, ByteUtils.toBytes(channels));
            result = ByteUtils.merger(result, ByteUtils.toBytes(sampleRate));
            result = ByteUtils.merger(result, ByteUtils.toBytes(byteRate));
            result = ByteUtils.merger(result, ByteUtils.toBytes(blockAlign));
            result = ByteUtils.merger(result, ByteUtils.toBytes(sampleBits));
            result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkId));
            result = ByteUtils.merger(result, ByteUtils.toBytes(dataChunkSize));
            return result;
        }
    }
    

ByteUtils: https://github.com/zhaolewei/ZlwAudioRecorder/blob/master/recorderlib/src/main/java/com/zlw/main/recorderlib/utils/ByteUtils.java

四、PCM轉(zhuǎn)Wav

  1. WavUtils.java
public class WavUtils {
    private static final String TAG = WavUtils.class.getSimpleName();
        /**
         * 生成wav格式的Header
         * wave是RIFF文件結(jié)構(gòu),每一部分為一個chunk,其中有RIFF WAVE chunk,
         * FMT Chunk,F(xiàn)act chunk(可選),Data chunk
         *
         * @param totalAudioLen 不包括header的音頻數(shù)據(jù)總長度
         * @param sampleRate    采樣率,也就是錄制時使用的頻率
         * @param channels      audioRecord的頻道數(shù)量
         * @param sampleBits    位寬
         */
        public static byte[] generateWavFileHeader(int totalAudioLen, int sampleRate, int channels, int sampleBits) {
            WavHeader wavHeader = new WavHeader(totalAudioLen, sampleRate, (short) channels, (short) sampleBits);
            return wavHeader.getHeader();
        }
    }

    /**
     * 將header寫入到pcm文件中 不修改文件名
     *
     * @param file   寫入的pcm文件
     * @param header wav頭數(shù)據(jù)
     */
    public static void writeHeader(File file, byte[] header) {
        if (!FileUtils.isFile(file)) {
            return;
        }

        RandomAccessFile wavRaf = null;
        try {
            wavRaf = new RandomAccessFile(file, "rw");
            wavRaf.seek(0);
            wavRaf.write(header);
            wavRaf.close();
        } catch (Exception e) {
            Logger.e(e, TAG, e.getMessage());
        } finally {
            try {
                if (wavRaf != null) {
                    wavRaf.close();
                }
            } catch (IOException e) {
                Logger.e(e, TAG, e.getMessage());
            }
        }
  1. RecordHelper.java
private void makeFile() {
    mergePcmFiles(recordFile, files);

    //這里實現(xiàn)上一篇未完成的工作
    byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());
    WavUtils.writeHeader(resultFile, header);
    
    Logger.i(TAG, "錄音完成! path: %s ; 大?。?s", recordFile.getAbsoluteFile(), recordFile.length());
    }

參考鏈接:

  1. http://soundfile.sapp.org/doc/WaveFormat/
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容