目錄
- 什么是PCM?
- PCM數據格式
- FFmpeg支持的PCM數據格式
- FFmpeg中Packed和Planar的PCM數據區別
- 字節序
- PCM音頻數據的處理
- 參考
1. 什么是PCM?
PCM(Pulse Code Modulation,脈沖編碼調制)音頻數據是未經壓縮的音頻采樣數據裸流,它是由模擬信號經過采樣、量化、編碼轉換成的標準數字音頻數據。
描述PCM數據的6個參數:
- Sample Rate : 采樣頻率。8kHz(電話)、44.1kHz(CD)、48kHz(DVD)。
- Sample Size : 量化位數。通常該值為16-bit。
- Number of Channels : 通道個數。常見的音頻有立體聲(stereo)和單聲道(mono)兩種類型,立體聲包含左聲道和右聲道。另外還有環繞立體聲等其它不太常用的類型。
- Sign : 表示樣本數據是否是有符號位,比如用一字節表示的樣本數據,有符號的話表示范圍為-128 ~ 127,無符號是0 ~ 255。
- Byte Ordering : 字節序。字節序是little-endian還是big-endian。通常均為little-endian。字節序說明見第4節。
- Integer Or Floating Point : 整形或浮點型。大多數格式的PCM樣本數據使用整形表示,而在一些對精度要求高的應用方面,使用浮點類型表示PCM樣本數據。
推薦的PCM數據播放工具:
- ffplay, 使用示例如下:
//播放格式為f32le,單聲道,采樣頻率48000Hz的PCM數據
ffplay -f f32le -ac 1 -ar 48000 pcm_audio
- Audacity:一款免費開源的跨平臺音頻處理軟件。
- Adobe Auditon。導入原始數據,打開的時候需要選擇采樣率、格式和字節序。
2. PCM數據格式
如果是單聲道的音頻文件,采樣數據按時間的先后順序依次存入(有的時候也會采用LRLRLR方式存儲,只是另一個聲道的數據為0),如果是雙聲道的話就按照LRLRLR的方式存儲,存儲的時候與字節序有關。big-endian模式如下圖所示:
3. FFmpeg支持的PCM數據格式
使用ffmpeg -formats命令,獲取ffmpeg支持的音視頻格式,其中我們可以找到支持的PCM格式。
DE alaw PCM A-law
DE f32be PCM 32-bit floating-point big-endian
DE f32le PCM 32-bit floating-point little-endian
DE f64be PCM 64-bit floating-point big-endian
DE f64le PCM 64-bit floating-point little-endian
DE mulaw PCM mu-law
DE s16be PCM signed 16-bit big-endian
DE s16le PCM signed 16-bit little-endian
DE s24be PCM signed 24-bit big-endian
DE s24le PCM signed 24-bit little-endian
DE s32be PCM signed 32-bit big-endian
DE s32le PCM signed 32-bit little-endian
DE s8 PCM signed 8-bit
DE u16be PCM unsigned 16-bit big-endian
DE u16le PCM unsigned 16-bit little-endian
DE u24be PCM unsigned 24-bit big-endian
DE u24le PCM unsigned 24-bit little-endian
DE u32be PCM unsigned 32-bit big-endian
DE u32le PCM unsigned 32-bit little-endian
DE u8 PCM unsigned 8-bit
s是有符號,u是無符號,f是浮點數。
be是大端,le是小端。
4. FFmpeg中Packed和Planar的PCM數據區別
FFmpeg中音視頻數據基本上都有Packed和Planar兩種存儲方式,對于雙聲道音頻來說,Packed方式為兩個聲道的數據交錯存儲;Planar方式為兩個聲道分開存儲。假設一個L/R為一個采樣點,數據存儲的方式如下所示:
- Packed: L R L R L R L R
- Planar: L L L L R R R R
FFmpeg音頻解碼后的數據是存放在AVFrame結構中的。
- Packed格式,frame.data[0]或frame.extended_data[0]包含所有的音頻數據中。
- Planar格式,frame.data[i]或者frame.extended_data[i]表示第i個聲道的數據(假設聲道0是第一個), AVFrame.data數組大小固定為8,如果聲道數超過8,需要從frame.extended_data獲取聲道數據。
下面為FFmpeg內部存儲音頻使用的采樣格式,所有的Planar格式后面都有字母P標識。
enum AVSampleFormat {
AV_SAMPLE_FMT_NONE = -1,
AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
AV_SAMPLE_FMT_S16, ///< signed 16 bits
AV_SAMPLE_FMT_S32, ///< signed 32 bits
AV_SAMPLE_FMT_FLT, ///< float
AV_SAMPLE_FMT_DBL, ///< double
AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP, ///< float, planar
AV_SAMPLE_FMT_DBLP, ///< double, planar
AV_SAMPLE_FMT_S64, ///< signed 64 bits
AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};
說明:
- Planar模式是ffmpeg內部存儲模式,我們實際使用的音頻文件都是Packed模式的。
- FFmpeg解碼不同格式的音頻輸出的音頻采樣格式不是一樣。測試發現,其中AAC解碼輸出的數據為浮點型的 AV_SAMPLE_FMT_FLTP 格式,MP3解碼輸出的數據為 AV_SAMPLE_FMT_S16P 格式(使用的mp3文件為16位深)。具體采樣格式可以查看解碼后的AVFrame中的format成員或解碼器的AVCodecContext中的sample_fmt成員。
- Planar或者Packed模式直接影響到保存文件時寫文件的操作,操作數據的時候一定要先檢測音頻采樣格式。
5. 字節序
談到字節序的問題,必然牽涉到兩大CPU派系。那就是Motorola的PowerPC系列CPU和Intel的x86系列CPU。PowerPC系列采用big endian方式存儲數據,而x86系列則采用little endian方式存儲數據。那么究竟什么是big endian,什么又是little endian?
big endian是指低地址存放最高有效字節(MSB,Most Significant Bit),而little endian則是低地址存放最低有效字節(LSB,Least Significant Bit)。
下面用圖像加以說明。比如數字0x12345678在兩種不同字節序CPU中的存儲順序如下所示:
Big Endian
低地址 高地址
----------------------------------------------------------------------------->
| 12 | 34 | 56 | 78 |
Little Endian
低地址 高地址
----------------------------------------------------------------------------->
| 78 | 56 | 34 | 12 |
所有網絡協議都是采用big endian的方式來傳輸數據的。所以也把big endian方式稱之為網絡字節序。當兩臺采用不同字節序的主機通信時,在發送數據之前都必須經過字節序的轉換成為網絡字節序后再進行傳輸。
6. PCM音頻數據的處理
6.1 分離雙聲道PCM音頻數據左右聲道的數據
按照雙聲道的LRLRLR的PCM音頻數據可以通過將它們交叉的讀出來的方式來分離左右聲道的數據。
int pcm_s16le_split(const char* file, const char* out_lfile, const char* out_rfile) {
FILE *fp = fopen(file, "rb+");
if (fp == NULL) {
printf("open %s failed\n", file);
return -1;
}
FILE *fp1 = fopen(out_lfile, "wb+");
if (fp1 == NULL) {
printf("open %s failed\n", out_lfile);
return -1;
}
FILE *fp2 = fopen(out_rfile, "wb+");
if (fp2 == NULL) {
printf("open %s failed\n", out_rfile);
return -1;
}
char * sample = (char *)malloc(4);
while(!feof(fp)) {
fread(sample, 1, 4, fp);
//L
fwrite(sample, 1, 2, fp1);
//R
fwrite(sample + 2, 1, 2, fp2);
}
free(sample);
fclose(fp);
fclose(fp1);
fclose(fp2);
return 0;
}