聲音主觀感受上主要有響度、音高、音色以及掩蔽效應等特徵,其中響度、音高、音色在物理上可以量化成具有振幅、頻率、相位的波,故稱它們為聲音的"三要素"。
音高。表示人耳對音調高低的主觀感受,物理上用頻率與之對應,頻率越高,音高越高。人耳可以識別的聲音頻率範圍是 20~20kHz。音色。從音樂的角度來講,音色由樂器的材質決定。物理上,音色是是眾多相位不同波形疊加產生,其中波形的基頻產生的聽得最清楚的音稱為基音,各諧波(其它相位)微小震動產生的聲音稱為泛音。2. A/D轉換與PCM要將自然界中的信號進行傳輸,聲音轉換成計算機能夠識別的形式,前者稱為模擬信號,後者成為數位訊號,模擬信號與數位訊號之間的轉換過程就叫做數模轉換(A/D)。
PCM(Pulse Code Modulation) 是數字通信中編碼方式的一種,也即計算所能識別的信號形式。PCM 通過對模擬信號進行採樣、量化、編碼而產生,接收 PCM 信號的端則將編碼"還原"成模擬信號。採樣過程將連續的信號按照固定時間間隔離散化(聲音是一個連續信號),根據奈奎斯特採樣定理,為了保證最終的數位訊號能比較完整的還原成模擬信號,採樣頻率必須是原信號頻率的2倍及以上。採樣完成了信號在時間緯度上的離散,但仍是模擬信號,因為樣值在一定範圍內仍然具有無限多取值可能,所以將取值範圍按照一定步長劃分為有限個取值,這就是量化。將量化後的樣值按照一定規則排列就是編碼了。
簡單來說,A/D 轉換就是把連續變成離散,把無限變成有限。
PCM Encoded Signal在音視頻領域,PCM 常用來保存原始音頻數據,並且約定 PCM 等價於無損編碼。很多高保真的音頻也都採用 PCM 保存,缺點就是佔用空間會比較多一些。
3. 音頻編碼3.1. 音頻參數音頻數據參數有採樣率、採樣位數以及聲道數。
採樣率指聲音信號在 A/D 轉換過程中單位時間內的採樣次數,單位是 Hz; 採樣位數是指用多少 bit 數據對聲音進行量化,常用的有 8 bit、16bit; 聲道數又稱音軌,不準確地理解是聲源,人聽到聲音時會對聲源進行定位,不同位置的聲道數越多,效果就越逼真,常見聲道數有:
雙聲道,stereo,最常見的類型,包含左聲道以及右聲道。5.1聲道,包含一個正面聲道、左前方聲道、右前方聲道、左環繞聲道、右環繞聲道、一個低音聲道,最早應用於早期的電影院。7.1聲道,在5.1聲道的基礎上,把左右的環繞聲道拆分為左右環繞聲道以及左右後置聲道,主要應用於BD以及現代的電影院。若音頻PCM格式描述為 44100kHz 16LE stereo,意思是採樣率是 44100Hz,採樣位數是 16bit(無符號數)並且單個採樣採用小端法存儲,stereo 表示雙聲道。
量化的值可能是整數也可能是浮點數。
3.2. 音頻PCM存儲如果是單聲道音頻,那麼採樣數據按照時間順序一次存儲。如果音頻是雙聲道,則左右聲道採樣按照時間順序交錯存儲。
當使用 ffplay 播放 PCM 數據時,需要指定音頻的採樣率、採樣位寬以及聲道數:
ffplay -autoexit -ar 44100 -channels 2 -f s16le -i raw.pcm
3.3. WAV 格式原始PCM數據的一個問題就是每次播放時需要顯式指定採樣率等參數,比較直接的解決方案就是將這些參數也寫入音頻文件,讓播放器幫我們解析這些參數。WAV 是 Microsoft 和 IBM 為 PC 開發的音頻文件格式,它做的事情就是在文件頭部寫入一些描述信息,用來告訴播放器所需要的參數。
WAV 採用 RIFF 規範進行數據存儲:
WAV format第一列表示對應區塊是採用大端法還是小端法進行存儲; 第二列是區塊在文件中的偏移位置,第四列是每個區塊的大小,限定了區塊的固定佔用空間; 中間第三列就是每個區塊的定義,規定了每個區塊需要存放什麼數據。
圖中每個 Chunk 都有 ChunkID 和 ChunkSize,後面的 Chunk 都是第一個 Chunk 的 SubChunk。
第一個 ChunkID 內容是 "RIFF",指明文件存儲格式,隨後 ChunkSize 內容是剩餘文件長度(byte,4+(4+4+SubChunk1Size)+(4+4+SubChunk2Size))。Format 中存儲 "WAVE",表明這個文件存儲的是 PCM 數據,確定了 Format 也就決定了如何解析剩餘文件內容。
第二個 Chunk 描述了音頻參數。AudioFormat 指明音頻數據格式,PCM = 1,如果不是 1 就表示音頻數據是其它相應的壓縮格式(比如 MP3)。BlockAlign = NumChannels * BitsPerSample/8,每次對音頻數據的讀寫大小必須是 BlockAlign 的整數倍,並且只能從一個完整的 Block 的起始地址開始讀寫,從其它位置開始讀寫都是非法的。
前兩個 Chunk 描述了音頻數據的基本信息,第三個 Chunk 存儲實際音頻數據。
3.4. 讀寫 WAV下面演示如何將 PCM 存儲為 WAV 文件以及如何從 WAV 文件中讀取出原始 PCM 數據並列印音頻參數。
pcm 文件使用 ffmpeg -i audio.mp3 -f s16le -acodec pcm_s16le out.pcm 從音頻文件中提取。
/**
* @param0 executable program's file path
* @param1 raw pcm data file path
* @param2 sample rate
* @param3 sample size in bits
* @param4 number of channels
* @param4 output file path
*/
void write_wav(int argc, char const *argv[])
{
std::string pcm_file = std::string(argv[1]);
int sample_rate = std::atoi(argv[2]);
int sample_size = std::atoi(argv[3]);
int nr_channels = std::atoi(argv[4]);
std::string wav_file = std::string(argv[5]);
// read/write in binary mode
std::ifstream pcm_st;
pcm_st.exceptions(std::fstream::failbit | std::fstream::badbit);
try
{
pcm_st.open(pcm_file, std::ios::in | std::ios::binary);
}
catch (const std::fstream::failure &e)
{
LOG_E("Failed to open: %s-%d %s", argv[1], pcm_st.fail(), e.what());
return;
}
std::ofstream wav_st;
wav_st.exceptions(std::fstream::failbit | std::fstream::badbit);
try
{
wav_st.open(wav_file, std::ios::out | std::ios::binary);
}
catch (const std::fstream::failure &e)
{
LOG_E(e.what());
}
// define Header Chunk
ChunkHeader wav_header{};
memcpy(wav_header.ChunkID, "RIFF", strlen("RIFF"));
memcpy(wav_header.Format, "WAVE", strlen("WAVE"));
wav_st.seekp(sizeof(ChunkHeader), std::ios::cur);
// define SubChunk1
ChunkFmt wav_fmt{};
memcpy(wav_fmt.ChunkID, "fmt ", strlen("fmt "));
wav_fmt.ChunkSize = sizeof(wav_fmt) - 8;
wav_fmt.AudioFormat = 1;
wav_fmt.NrChannels = nr_channels;
wav_fmt.SampleRate = sample_rate;
wav_fmt.ByteRate = sample_rate * nr_channels * sample_size / 8;
wav_fmt.BlockAlign = nr_channels * sample_size / 8;
wav_fmt.BitsPerSample = sample_size;
wav_st.seekp(sizeof(ChunkFmt), std::ios::cur);
// define SubChunk2
ChunkData wav_data{};
memcpy(wav_data.ChunkID, "data", strlen("data"));
wav_st.seekp(sizeof(ChunkData), std::ios::cur);
LOG_I("Writing pcm data");
int total_raw_data_size = 0;
int readed_size = 0;
char *buffer = static_cast<char *>(malloc(1024));
try
{
do
{
pcm_st.read(buffer, 1024);
readed_size = pcm_st.gcount();
total_raw_data_size += readed_size;
wav_st.write(buffer, readed_size);
} while (pcm_st.gcount() > 0);
}
catch (const std::fstream::failure &e)
{
LOG_E("Oops, some error occured: %s", e.what());
}
wav_header.ChunkSize = 4 + sizeof(ChunkFmt) + sizeof(ChunkData) + total_raw_data_size;
wav_data.ChunkSize = total_raw_data_size;
// seek at the beggining of the output file
wav_st.seekp(0, std::ios::beg);
// write the header
LOG_I("Writing header chunk");
wav_st.write(reinterpret_cast<char *>(wav_header.ChunkID), sizeof(char) * 4);
wav_st.write(reinterpret_cast<char *>(&wav_header.ChunkSize), sizeof(uint32_t));
wav_st.write(reinterpret_cast<char *>(wav_header.Format), sizeof(char) * 4);
LOG_I("Writing format chunk");
wav_st.write(reinterpret_cast<char *>(wav_fmt.ChunkID), sizeof(char) * 4);
wav_st.write(reinterpret_cast<char *>(&wav_fmt.ChunkSize), sizeof(uint32_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.AudioFormat), sizeof(uint16_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.NrChannels), sizeof(uint16_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.SampleRate), sizeof(uint32_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.ByteRate), sizeof(uint32_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.BlockAlign), sizeof(uint16_t));
wav_st.write(reinterpret_cast<char *>(&wav_fmt.BitsPerSample), sizeof(uint16_t));
LOG_I("Writing data chunk");
wav_st.write(reinterpret_cast<char *>(wav_data.ChunkID), sizeof(char) * 4);
wav_st.write(reinterpret_cast<char *>(&wav_data.ChunkSize), sizeof(uint32_t));
pcm_st.close();
wav_st.close();
LOG_I("completed, %s", argv[5]);
}
/**
* @param0 executable program's file path
* @param1 wav file path
* @param2 output file path
*/
void read_wav(int argc, char const *argv[])
{
std::string wav_file = std::string(argv[1]);
std::string pcm_file = std::string(argv[2]);
// read/write in binary mode
std::ifstream wav_st;
wav_st.exceptions(std::fstream::failbit | std::fstream::badbit);
try
{
wav_st.open(wav_file, std::ios::in | std::ios::binary);
}
catch (const std::fstream::failure &e)
{
LOG_E(e.what());
}
std::ofstream pcm_st;
pcm_st.exceptions(std::fstream::failbit | std::fstream::badbit);
try
{
pcm_st.open(pcm_file, std::ios::out | std::ios::binary);
}
catch (const std::fstream::failure &e)
{
LOG_E("Failed to open: %s-%d %s", argv[1], pcm_st.fail(), e.what());
return;
}
ChunkHeader wav_header{};
ChunkFmt wav_fmt{};
ChunkData wav_data{};
// read RIFF chunk
LOG_I("Reading RIFF chunk");
wav_st.read(reinterpret_cast<char *>(wav_header.ChunkID), sizeof(char) * 4);
wav_st.read(reinterpret_cast<char *>(&wav_header.ChunkSize), sizeof(uint32_t));
wav_st.read(reinterpret_cast<char *>(wav_header.Format), sizeof(char) * 4);
if (std::string(wav_header.Format) != "WAVE")
{
LOG_E("Invalid format: %s", wav_header.Format);
return;
}
// read fmt chunk
LOG_I("Reading fmt chunk");
wav_st.read(reinterpret_cast<char *>(wav_fmt.ChunkID), sizeof(char) * 4);
wav_st.read(reinterpret_cast<char *>(&wav_fmt.ChunkSize), sizeof(uint32_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.AudioFormat), sizeof(uint16_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.NrChannels), sizeof(uint16_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.SampleRate), sizeof(uint32_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.ByteRate), sizeof(uint32_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.BlockAlign), sizeof(uint16_t));
wav_st.read(reinterpret_cast<char *>(&wav_fmt.BitsPerSample), sizeof(uint16_t));
// read data chunk
LOG_I("Reading data chunk");
wav_st.read(reinterpret_cast<char *>(wav_data.ChunkID), sizeof(char) * 4);
wav_st.read(reinterpret_cast<char *>(&wav_data.ChunkSize), sizeof(uint32_t));
LOG_I("Reading pcm data");
int total_raw_data_size = 0;
int readed_size = 0;
char *buffer = static_cast<char *>(malloc(1024));
try
{
do
{
wav_st.read(buffer, 1024);
readed_size = wav_st.gcount();
total_raw_data_size += readed_size;
pcm_st.write(buffer, readed_size);
} while (wav_st.gcount() > 0);
}
catch (const std::fstream::failure &e)
{
LOG_E("Oops, some error occured: %s", e.what());
}
pcm_st.close();
wav_st.close();
LOG_I("completed, %s", argv[2]);
}程序運行執行後可以通過 ffplay 測試音頻是否能正常播放:
# 寫 pcm 寫入 wav 文件
ffplay -autoexit -i build/out.wav
# 從 wav 文件提取 pcm
ffplay -autoexit -ar 44100 -channels 2 -f s16le -i build/out.pcm
4. References[1] 聲音"三要素": https://blog.csdn.net/junllee/article/details/7217435
[2] 音頻屬性相關: https://www.cnblogs.com/yongdaimi/p/10722355.html
[3] WAV: https://zh.wikipedia.org/wiki/WAV
[4] WAVE PCM soundfile format: http://soundfile.sapp.org/doc/WaveFormat