什么是汉宁窗?
汉宁窗是一种在信号处理和数据分析中常用的窗函数,它的主要作用是减少频谱泄漏。

频谱泄漏是什么?
当你对一个无限长的信号进行傅里叶变换时,可以得到完美的频谱,但在计算机中,我们只能处理有限长的数据段,这相当于用一个矩形窗去截取原始信号,矩形窗的边缘非常陡峭,在频域中会产生很宽的旁瓣,这些旁瓣会“泄漏”到主瓣的附近,从而掩盖掉附近频率的弱信号,或者使强信号的频谱变得模糊。
汉宁窗如何解决?
汉宁窗是一个两端平滑过渡到零的窗函数,它的形状像一个倒置的U形,使用汉宁窗后,信号的两端会逐渐衰减到零,这大大减小了频域中的旁瓣,使得频谱分析更清晰、更准确,这是以牺牲主瓣宽度(频率分辨率)为代价的。
汉宁窗的数学公式
汉宁窗的离散形式(用于数字信号处理)公式如下:
*`w(n) = 0.5 (1 - cos(2 n / (N - 1)))`**

w(n)是窗函数在第n个点的值。N是窗的长度(即你数据段的点数)。n是当前点的索引,从0到N-1。
C语言实现
下面我们分步讲解如何在C语言中实现汉宁窗。
步骤1:包含必要的头文件
我们需要 <math.h> 来使用 cos() 和 M_PI (圆周率) 函数。
#include <stdio.h> #include <math.h> // 用于数学函数
步骤2:创建汉宁窗生成函数
这个函数将接收窗的长度 N,并返回一个动态分配的、包含汉宁窗系数的数组。
/**
* @brief 生成汉宁窗系数
* @param N 窗的长度
* @return 指向汉宁窗系数数组的指针,失败时返回NULL
*/
double* generate_hanning_window(int N) {
// 检查输入是否有效
if (N <= 0) {
return NULL;
}
// 为窗系数分配内存
double* window = (double*)malloc(N * sizeof(double));
if (window == NULL) {
// 内存分配失败
return NULL;
}
// 计算并填充窗系数
for (int n = 0; n < N; n++) {
// 应用汉宁窗公式
window[n] = 0.5 * (1.0 - cos(2.0 * M_PI * n / (N - 1)));
}
return window;
}
步骤3:使用窗函数处理数据
现在我们有了窗系数,可以将其应用到我们的数据上,这通常被称为“加窗”操作。

/**
* @brief 对数据应用汉宁窗
* @param data 输入数据数组
* @param window 窗函数数组
* @param N 数据和窗的长度
*/
void apply_window(double* data, const double* window, int N) {
for (int i = 0; i < N; i++) {
data[i] *= window[i];
}
}
完整示例代码
下面是一个完整的、可运行的C程序,它演示了如何生成汉宁窗并将其应用到一组示例数据上。
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
// 函数声明
double* generate_hanning_window(int N);
void apply_window(double* data, const double* window, int N);
void print_array(const char* label, const double* arr, int N);
int main() {
int N = 10; // 窗的长度和数据长度
// 1. 生成汉宁窗
double* hanning_win = generate_hanning_window(N);
if (hanning_win == NULL) {
fprintf(stderr, "错误:无法生成汉宁窗,\n");
return 1;
}
print_array("生成的汉宁窗:", hanning_win, N);
// 2. 创建一些示例数据 (一个正弦波)
double original_data[N];
for (int i = 0; i < N; i++) {
original_data[i] = sin(2.0 * M_PI * i / N); // 一个周期的正弦波
}
print_array("原始数据:", original_data, N);
// 3. 对数据进行备份,以便比较
double windowed_data[N];
for (int i = 0; i < N; i++) {
windowed_data[i] = original_data[i];
}
// 4. 应用汉宁窗
apply_window(windowed_data, hanning_win, N);
print_array("加窗后的数据:", windowed_data, N);
// 5. 释放动态分配的内存
free(hanning_win);
return 0;
}
/**
* @brief 生成汉宁窗系数
* @param N 窗的长度
* @return 指向汉宁窗系数数组的指针,失败时返回NULL
*/
double* generate_hanning_window(int N) {
if (N <= 0) {
return NULL;
}
double* window = (double*)malloc(N * sizeof(double));
if (window == NULL) {
return NULL;
}
for (int n = 0; n < N; n++) {
window[n] = 0.5 * (1.0 - cos(2.0 * M_PI * n / (N - 1)));
}
return window;
}
/**
* @brief 对数据应用汉宁窗
* @param data 输入数据数组
* @param window 窗函数数组
* @param N 数据和窗的长度
*/
void apply_window(double* data, const double* window, int N) {
for (int i = 0; i < N; i++) {
data[i] *= window[i];
}
}
/**
* @brief 打印数组
*/
void print_array(const char* label, const double* arr, int N) {
printf("%s\n", label);
for (int i = 0; i < N; i++) {
printf("%.4f ", arr[i]);
}
printf("\n\n");
}
编译和运行
你需要使用一个支持C99标准的编译器(比如GCC),并链接数学库。
gcc hanning_window_example.c -o hanning_example -lm ./hanning_example
预期输出
你会看到三组数据:
-
生成的汉宁窗: 从0开始平滑地增加到1,再平滑地减少到0。
生成的汉宁窗: 0.0000 0.0669 0.2265 0.4604 0.6935 0.8660 0.9604 0.9933 1.0000 0.9933(注意:对于N=10,最后一个点N-1=9,`cos(2PI9/9) = cos(2PI) = 1`,所以值为0.5(1-1)=0,我的公式里是n/N-1,所以最后一个点应该是0,上面的代码是正确的,这里打印的可能是另一个变体,比如n/N,让我们修正一下代码以匹配标准公式。)
修正后的标准汉宁窗 (N=10):
n=0:5 * (1 - cos(0)) = 0n=1:5 * (1 - cos(2*PI*1/9)) ≈ 0.067...n=9:5 * (1 - cos(2*PI*9/9)) = 0.5 * (1 - cos(2*PI)) = 0所以标准汉宁窗在两端都是0,上面的代码实现是正确的。
-
原始数据: 一个标准的正弦波。
-
加窗后的数据: 正弦波的振幅被汉宁窗的形状调制,两端的数据被“压扁”了,接近于0。
其他注意事项
- 对称性: 汉宁窗关于其中心点对称,这对于很多信号处理应用很重要,因为它可以保持信号的相位特性。
- 窗函数的选择: 汉宁窗是很好的通用选择,但并非唯一,还有其他窗函数,如汉明窗、布莱克曼窗、平顶窗等,它们在旁瓣抑制和主瓣宽度之间有不同的权衡。
- 汉明窗:
w(n) = 0.54 - 0.46 * cos(2*PI*n/(N-1)),旁瓣抑制比汉宁窗更好,但主瓣稍宽。 - 布莱克曼窗: 旁瓣抑制更好,但主瓣更宽,频率分辨率更低。
- 汉明窗:
- 内存管理: 使用
malloc动态分配内存后,必须使用free来释放,否则会导致内存泄漏,在更复杂的程序中,应该检查malloc是否返回NULL(内存不足)。 - 替代方案: 如果你正在使用像FFTW或Librosa这样的科学计算库,它们通常内置了高效的窗函数生成函数,可以直接调用,比自己实现更方便、更可靠。
