播放器是無法直接播放PCM的,因為播放器並不知道PCM的采樣率、聲道數、位深度等參數。當PCM轉成某種特定的音頻文件格式后(比如轉成WAV),就能夠被播放器識別播放了。
本文通過2種方式(命令行、編程)演示一下:如何將PCM轉成WAV。
WAV文件格式
在進行PCM轉WAV之前,先再來認識一下WAV的文件格式。
- WAV、AVI文件都是基於RIFF標准的文件格式
- RIFF(Resource Interchange File Format,資源交換文件格式)由Microsoft和IBM提出
- 所以WAV、AVI文件的最前面4個字節都是RIFF四個字符
找遍了全網,並沒有找到令我十分滿意的WAV文件格式圖,於是按照自己的理解畫了一張表格,個人覺得還是極其通俗易懂的。
每一個chunk(數據塊)都由3部分組成:
- id:chunk的標識
- data size:chunk的數據部分大小,字節為單位
- data,chunk的數據部分
整個WAV文件是一個RIFF chunk,它的data由3部分組成:
- format:文件類型
- fmt chunk
- 音頻參數相關的chunk
- 它的data里面有采樣率、聲道數、位深度等參數信息
- data chunk
- 音頻數據相關的chunk
- 它的data就是真正的音頻數據(比如PCM數據)
RIFF chunk除去data chunk的data(音頻數據)后,剩下的內容可以稱為:WAV文件頭,一般是44字節。
命令行
通過下面的命令可以將PCM轉成WAV。
ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm out.wav
需要注意的是:上面命令生成的WAV文件頭有78字節。對比44字節的文件頭,它多增加了一個34字節大小的LIST chunk。
關於LIST chunk的參考資料:
加上一個輸出文件參數-bitexact可以去掉LIST Chunk。
ffmpeg -ar 44100 -ac 2 -f s16le -i out.pcm -bitexact out2.wav
編程
在PCM數據的前面插入一個44字節的WAV文件頭,就可以將PCM轉成WAV。
WAV的文件頭結構
WAV的文件頭結構大概如下所示:
#define AUDIO_FORMAT_PCM 1
#define AUDIO_FORMAT_FLOAT 3
// WAV文件頭(44字節)
typedef struct {
// RIFF chunk的id
uint8_t riffChunkId[4] = {'R', 'I', 'F', 'F'};
// RIFF chunk的data大小,即文件總長度減去8字節
uint32_t riffChunkDataSize;
// "WAVE"
uint8_t format[4] = {'W', 'A', 'V', 'E'};
/* fmt chunk */
// fmt chunk的id
uint8_t fmtChunkId[4] = {'f', 'm', 't', ' '};
// fmt chunk的data大小:存儲PCM數據時,是16
uint32_t fmtChunkDataSize = 16;
// 音頻編碼,1表示PCM,3表示Floating Point
uint16_t audioFormat = AUDIO_FORMAT_PCM;
// 聲道數
uint16_t numChannels;
// 采樣率
uint32_t sampleRate;
// 字節率 = sampleRate * blockAlign
uint32_t byteRate;
// 一個樣本的字節數 = bitsPerSample * numChannels >> 3
uint16_t blockAlign;
// 位深度
uint16_t bitsPerSample;
/* data chunk */
// data chunk的id
uint8_t dataChunkId[4] = {'d', 'a', 't', 'a'};
// data chunk的data大小:音頻數據的總長度,即文件總長度減去文件頭的長度(一般是44)
uint32_t dataChunkDataSize;
} WAVHeader;
PCM轉WAV核心實現
封裝到了FFmpegs類的pcm2wav函數中。
#include <QFile>
#include <QDebug>
class FFmpegs {
public:
FFmpegs();
static void pcm2wav(WAVHeader &header,
const char *pcmFilename,
const char *wavFilename);
};
void FFmpegs::pcm2wav(WAVHeader &header,
const char *pcmFilename,
const char *wavFilename) {
header.blockAlign = header.bitsPerSample * header.numChannels >> 3;
header.byteRate = header.sampleRate * header.blockAlign;
// 打開pcm文件
QFile pcmFile(pcmFilename);
if (!pcmFile.open(QFile::ReadOnly)) {
qDebug() << "文件打開失敗" << pcmFilename;
return;
}
header.dataChunkDataSize = pcmFile.size();
header.riffChunkDataSize = header.dataChunkDataSize
+ sizeof (WAVHeader) - 8;
// 打開wav文件
QFile wavFile(wavFilename);
if (!wavFile.open(QFile::WriteOnly)) {
qDebug() << "文件打開失敗" << wavFilename;
pcmFile.close();
return;
}
// 寫入頭部
wavFile.write((const char *) &header, sizeof (WAVHeader));
// 寫入pcm數據
char buf[1024];
int size;
while ((size = pcmFile.read(buf, sizeof (buf))) > 0) {
wavFile.write(buf, size);
}
// 關閉文件
pcmFile.close();
wavFile.close();
}
調用函數
// 封裝WAV的頭部
WAVHeader header;
header.numChannels = 2;
header.sampleRate = 44100;
header.bitsPerSample = 16;
// 調用函數
FFmpegs::pcm2wav(header, "F:/in.pcm", "F:/out.wav");