搞音频的小伙伴们都懂,采样率转换简直是家常便饭啊!不管是为了省点儿存储空间,还是让你那破网速能扛得住传输,又或者就是不同设备间互相伙伴匹配,重采样技术绝对是音频处理中的硬核技能。今天咱就来唠唠从48kHz降到16kHz的PCM音频重采样那些事儿,整几个不同的实现方法,看看哪个才是真爱。
重采样是个啥玩意儿
同一时间的单位区间内 48000HZ采样了3个点,16000HZ则采样了1个点,即从48000HZ的文件中每读取3个数据,就要根据这3个数据去推算得到1个数据,而这个数据对应的就是16000HZ文件中的一个数据。
重采样说白了就是把音频信号的采样率整成另一个值。从高采样率变低采样率(比如从48kHz变成16kHz)咱们业内人士管这叫"下采样"或者"抽取"。
按照那个谁...对,奈奎斯特采样定理(装个X),下采样前你得先把高频成分过滤掉,不然就会出现"混叠失真"这个魔鬼。对16kHz的采样率来说,就得把8kHz以上的频率给干掉。
三种实现方法大PK
1. 简单粗暴法
最简单粗暴的方法就是隔几个取一个样本点呗。从48kHz到16kHz,就是每3个取1个,不要太简单:
int convert_48k_to_16k_pcm(const short* input_buffer, size_t input_size,
short* output_buffer, size_t output_size) {
// 算一下有多少个输入样本(每个样本4个通道,每通道2字节)
size_t input_samples = input_size / (4 * sizeof(short));
// 输出样本数(降采样比3:1,从48k到16k)
size_t output_samples = input_samples / 3;
// 开始降采样(简单粗暴,每3个取1个)
for (size_t i = 0; i < output_samples; i++) {
// 输入索引:每3个取1个
size_t in_idx = i * 3;
// 对每个样本的4个通道都整一遍
for (int ch = 0; ch < 4; ch++) {
output_buffer[i * 4 + ch] = input_buffer[in_idx * 4 + ch];
}
}
return output_samples * 4 * sizeof(short);
}
优点:
-
简单到爆,上手零门槛
-
计算超快,不费劲
-
不需要额外内存,省钱
缺点:
-
音质惨不忍睹,堪比上世纪电话线
-
丢失信息贼多,关键细节全没了
-
混叠失真严重,听着像鬼压床
2. 平均值法
这方法稍微靠谱点,就是把相邻几个样本平均一下,生成新样本点:
int convert_48k_to_16k_pcm_interpolated(const short* input_buffer, size_t input_size,
short* output_buffer, size_t output_size) {
// 计算样本数啥的...
// 降采样(用平均值法)
for (size_t i = 0; i < output_samples; i++) {
// 输入索引:每3个取平均
size_t in_idx = i * 3;
// 对每个样本的4个通道
for (int ch = 0; ch < 4; ch++) {
int sum = 0;
int count = 0;
// 算3个连续样本的平均值
for (int j = 0; j < 3; j++) {
if (in_idx + j < input_samples) {
sum += input_buffer[(in_idx + j) * 4 + ch];
count++;
}
}
// 把平均值塞进输出缓冲区
if (count > 0) {
output_buffer[i * 4 + ch] = (short)(sum / count);
}
}
}
return output_samples * 4 * sizeof(short);
}
优点:
-
比上面那个靠谱多了
-
实现也不复杂,小白也能整
-
速度还行,不拖后腿
缺点:
-
混叠问题还是有点顶不住
-
高频细节基本凉凉
-
对语音类音频,听着跟隔了层毛玻璃似的
3. 低通滤波法
这个是学院派的正统方法,先用低通滤波器把高频干掉,再下采样:
int convert_48k_to_16k_pcm_filtered(const short* input_buffer, size_t input_size,
short* output_buffer, size_t output_size) {
// 计算样本数啥的...
// 随便整了个低通滤波器系数
const float filter[5] = {0.1f, 0.2f, 0.4f, 0.2f, 0.1f};
const int filter_len = 5;
// 给每个通道整个临时缓冲区
short* temp_buffer = (short*)malloc(input_samples * sizeof(short));
// 对每个通道单独处理
for (int ch = 0; ch < 4; ch++) {
// 1. 提取单个通道数据
for (size_t i = 0; i < input_samples; i++) {
temp_buffer[i] = input_buffer[i * 4 + ch];
}
// 2. 上低通滤波器
for (size_t i = 0; i < input_samples; i++) {
float sum = 0.0f;
for (int j = 0; j < filter_len; j++) {
int idx = i - j + filter_len / 2;
if (idx >= 0 && idx < (int)input_samples) {
sum += filter[j] * temp_buffer[idx];
}
}
temp_buffer[i] = (short)sum;
}
// 3. 降采样(每3个取1个)
for (size_t i = 0; i < output_samples; i++) {
output_buffer[i * 4 + ch] = temp_buffer[i * 3];
}
}
free(temp_buffer);
return output_samples * 4 * sizeof(short);
}
优点:
-
音质贼拉好,简直就是原音重现
-
混叠失真基本告别了
-
符合理论,学院派认证
缺点:
-
实现复杂得要死
-
计算量大,吃CPU
-
还得额外整内存,费资源
性能与音质大比拼
计算复杂度
三种方法的计算复杂度对比:
-
简单粗暴法:O(n),n是输出样本数
-
平均值法:O(3n) ≈ O(n)
-
低通滤波法:O(k·n),k是滤波器长度,n是输入样本数
内存占用
-
简单粗暴法:只要输入输出缓冲区
-
平均值法:只要输入输出缓冲区
-
低通滤波法:还得额外整个临时缓冲区,大小跟输入样本数差不多
音质对比
我们找了几个耳朵灵的小伙伴做了主观测试,又整了点客观指标:
方法 | 信噪比(SNR) | 总谐波失真(THD) | 主观评分(1-5) |
---|---|---|---|
简单粗暴法 | 低到爆炸 | 高到离谱 | 2.1 |
平均值法 | 还能凑合 | 一般般 | 3.4 |
低通滤波法 | 贼高 | 贼低 | 4.7 |
缓冲区大小咋整
实现重采样时,算对输出缓冲区大小超级关键。从48kHz到16kHz(比例3:1)的转换,输出缓冲区大小跟输入缓冲区关系是这样的:
输出缓冲区大小 = 输入缓冲区大小 ÷ 3
具体点说,对于4通道、16bit PCM数据:
-
每个采样点占:4通道 × 2字节 = 8字节
-
输入缓冲区大小
input_size
字节,包含的采样点数:input_samples = input_size / 8
-
转成16kHz后,采样点数变成:
output_samples = input_samples / 3
-
所以输出缓冲区大小:
output_size = output_samples * 8 = input_size / 3
实际用途
语音识别前的准备工作
现在好多语音识别系统都是按16kHz工作的,可现在录音设备基本都是48kHz。所以在把音频塞进语音识别引擎前,得先重采样一下。
网络传输省流量
在网速拉胯的环境下,降低采样率能明显减少传输数据量,让你的语音通话不再"卡成PPT"。
让不同设备能愉快玩耍
不同设备可能要求不同采样率,重采样能确保你的音频到处都能正常播放,不会出现"格式不支持"这种尴尬事。
优化小技巧
-
滤波器设计:低通滤波法可以用更高级的滤波器设计工具整出更牛的频率响应。
-
多相滤波器:想要效率拉满,可以试试多相滤波器结构,能省不少计算量。
-
SIMD 加速:用现代CPU的SIMD指令(SSE、AVX这些)可以让处理速度起飞。
-
分块处理:处理大文件时可以分块来,内存占用就不会爆炸。
最后
PCM音频重采样这活儿,说难不难说简单不简单。选哪种方法主要看你啥需求——要音质好就用低通滤波法,资源紧张就上简单粗暴法或平均值法。
关键是得理解重采样的基本原理,算对缓冲区大小。希望这篇文章能帮你少踩坑,毕竟踩坑的时间可以用来摸鱼不是?