音频编码—PCM
1. 声音三要素
声音主观感受上主要有响度、音高、音色以及掩蔽效应等特征,其中响度、音高、音色在物理上可以量化成具有振幅、频率、相位的波,故称它们为声音的”三要素”。
- 响度。表示声音能量的强弱,振幅越大,能量越大。
- 音高。表示人耳对音调高低的主观感受,物理上用频率与之对应,频率越高,音高越高。人耳可以识别的声音频率范围是 20~20kHz。
- 音色。从音乐的角度来讲,音色由乐器的材质决定。物理上,音色是是众多相位不同波形叠加产生,其中波形的基频产生的听得最清楚的音称为基音,各谐波(其它相位)微小震动产生的声音称为泛音。
2. A/D转换与PCM
要将自然界中的信号进行传输,声音转换成计算机能够识别的形式,前者称为模拟信号,后者称为数字信号,模拟信号与数字信号之间的转换过程就叫做数模转换(A/D)。
PCM(Pulse Code Modulation) 是数字通信中编码方式的一种,也即计算所能识别的信号形式。PCM 通过对模拟信号进行采样、量化、编码而产生,接收 PCM 信号的端则将编码”还原”成模拟信号。采样过程将连续的信号按照固定时间间隔离散化(声音是一个连续信号),根据奈奎斯特采样定理,为了保证最终的数字信号能比较完整的还原成模拟信号,采样频率必须是原信号频率的2倍及以上。采样完成了信号在时间纬度上的离散,但仍是模拟信号,因为样值在一定范围内仍然具有无限多取值可能,所以将取值范围按照一定步长划分为有限个取值,这就是量化。将量化后的样值按照一定规则排列就是编码了。
简单来说,A/D 转换就是将连续变成离散,将无限变成有限。

在音视频领域,PCM 常用来保存原始音频数据,并且约定 PCM 等价于无损编码。很多高保真的音频也都采用 PCM 保存,缺点就是占用空间会比较多一些。
3. 音频编码
3.1. 音频参数
音频数据参数有采样率、采样位数以及声道数。
采样率指声音信号在 A/D 转换过程中单位时间内的采样次数,单位是 Hz; 采样位数是指用多少 bit 数据对声音进行量化,常用的有 8 bit、16bit; 声道数又称音轨,不准确地理解是声源,人听到声音时会对声源进行定位,不同位置的声道数越多,效果就越逼真,常见声道数有:
- 单声道,mono。
- 双声道,stereo,最常见的类型,包含左声道以及右声道。
- 2.1声道,在双声道基础上加入一个低音声道。
- 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.pcm3.3. WAV 格式
原始PCM数据的一个问题就是每次播放时需要显式指定采样率等参数,比较直接的解决方案就是将这些参数也写入音频文件,让播放器帮我们解析这些参数。WAV 是 Microsoft 和 IBM 为 PC 开发的音频文件格式,它做的事情就是在文件头部写入一些描述信息,用来告诉播放器所需要的参数。
WAV 采用 RIFF 规范进行数据存储:

第一列表示对应区块是采用大端法还是小端法进行存储; 第二列是区块在文件中的偏移位置,第四列是每个区块的大小,限定了区块的固定占用空间; 中间第三列就是每个区块的定义,规定了每个区块需要存放什么数据。
图中每个 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