Android 音頻開發(fā) 目錄
- Android音頻開發(fā)(1):音頻相關(guān)知識
- Android音頻開發(fā)(2):使用AudioRecord錄制pcm格式音頻
- Android音頻開發(fā)(3):使用AudioRecord實現(xiàn)錄音的暫停和恢復(fù)
- Android音頻開發(fā)(4):PCM轉(zhuǎn)WAV格式音頻
- Android音頻開發(fā)(5):Mp3的錄制 - 編譯Lame源碼
- Android音頻開發(fā)(6):Mp3的錄制 - 使用Lame實時錄制MP3格式音頻
- 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 生成頭文件
- 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; } }
四、PCM轉(zhuǎn)Wav
- 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());
}
}
- 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());
}