1. 声音三要素

声音主观感受上主要有响度、音高、音色以及掩蔽效应等特征,其中响度、音高、音色在物理上可以量化成具有振幅、频率、相位的波,故称它们为声音的”三要素”。

  1. 响度。表示声音能量的强弱,振幅越大,能量越大。
  2. 音高。表示人耳对音调高低的主观感受,物理上用频率与之对应,频率越高,音高越高。人耳可以识别的声音频率范围是 20~20kHz。
  3. 音色。从音乐的角度来讲,音色由乐器的材质决定。物理上,音色是是众多相位不同波形叠加产生,其中波形的基频产生的听得最清楚的音称为基音,各谐波(其它相位)微小震动产生的声音称为泛音。

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; 声道数又称音轨,不准确地理解是声源,人听到声音时会对声源进行定位,不同位置的声道数越多,效果就越逼真,常见声道数有:

  • 单声道,mono。
  • 双声道,stereo,最常见的类型,包含左声道以及右声道。
  • 2.1声道,在双声道基础上加入一个低音声道。
  • 5.1声道,包含一个正面声道、左前方声道、右前方声道、左环绕声道、右环绕声道、一个低音声道,最早应用于早期的电影院。
  • 7.1声道,在5.1声道的基础上,把左右的环绕声道拆分为左右环绕声道以及左右后置声道,主要应用于BD以及现代的电影院。

若音频PCM格式描述为 44100kHz 16LE stereo,意思是采样率是 44100Hz,采样位数是 16bit(无符号数)并且单个采样采用小端法存储,stereo 表示双声道。

量化的值可能是整数也可能是浮点数。

3.2. 音频PCM存储

如果是单声道音频,那么采样数据按照时间顺序一次存储。如果音频是双声道,则左右声道采样按照时间顺序交错存储。

PCM Data

当使用 ffplay 播放 PCM 数据时,需要指定音频的采样率、采样位宽以及声道数:

1
$ 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 从音频文件中提取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
* @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 测试音频是否能正常播放:

1
2
3
4
# 写 pcm 写入 wav 文件
$ ffplay -autoexit -i build/out.wav
# 从 wav 文件提取 pcm
$ ffplay -autoexit -ar 44100 -channels 2 -f s16le -i build/out.pcm

4. 参考文章

声音”三要素”

音频属性相关

WAV

WAVE PCM soundfile format