音频频谱 via FFT

频谱和均衡器

频谱和均衡器,几乎是媒体播放程序的必备物件,没有这两个功能的媒体播放程序会被认为不够专业。
audio spectrum

声音信号的时域和频域

  • 时域
    是描述数学函数或物理信号对时间的关系。例如一个信号的时域波形可以表达信号随着时间的变化。
  • 频域
    是指在对函数或信号进行分析时,分析其和频率有关的部份,而不是和时间有关的部份。例如傅里叶变换可以将一个时域信号转换成在不同频率下对应的振幅及相位,其频谱就是时域信号在频域下的表现,而反傅里叶变换可以将频谱再转换回时域的信号。
    fft
    freq vs time
    或者到 这里 体验一下不同频率成分组成的波形。

FFT

FFT 是离散傅立叶变换的快速算法,可以将一个信号变换到频域。有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了。这就是很多信号分析采用 FFT 变换的原因。

一个模拟信号,经过 ADC 采样之后,就变成了数字信号。采样定理告诉我们,采样频率要大于信号频率的两倍。采样得到的数字信号,就可以做 FFT 变换了。N 个采样点,经过 FFT 之后,就可以得到 N 个点的 FFT 结果。为了方便进行 FFT 运算,通常 N 取 2 的整数次方。

假设采样频率为 Fs,信号频率 F,采样点数为 N。那么 FFT 之后结果就是一个为 N 点的复数。每一个点就对应着一个频率点。这个点的模值,就是该频率值下的幅度特性。而每个点的相位呢,就是在该频率下的信号的相位。第一个点表示直流分量(即 0Hz),而最后一个点 N 的再下一个点(实际上这个点是不存在的,这里是假设的第 N+1 个点)则表示采样频率 Fs,这中间被 N-1 个点平均分成 N 等份,每个点的频率依次增加。例如某点 n 所表示的频率为:Fn = (n - 1) x Fs / N。由此公式可以看出,Fn 所能分辨到的频率为 Fs / N,如果采样频率 Fs 为 1024Hz,采样点数为 1024 点,则可以分辨到 1Hz。1024Hz 的采样率采样 1024 点,刚好是 1 秒,也就是说,采样 1 秒时间的信号并做 FFT ,则结果可以分析到 1Hz,如果采样 2 秒时间的信号并做 FFT ,则结果可以分析到 0.5Hz。如果要提高频率分辨力,则必须增加采样点数,也即采样时间。频率分辨率和采样时间是倒数关系。

AudioSpectrum sample

工作流程

在这里插入图片描述

源代码

_readAudioData 函数

读取 PCM 数据并转换成浮点类型。

// read as float format (between -1.0 ~ 1.0)
HRESULT _readAudioData( BYTE* pData, DWORD dataLen, WAVEFORMATEX &wavFmt, float* buffer )
{
    for (int i = 0; i < FFT_BLOCK_SIZE; i++) {
        if (g_dataPos < dataLen) {
            switch (wavFmt.nChannels) {
            case 1:
                switch (wavFmt.wBitsPerSample) {
                case 8:
                    buffer[i] = pData[g_dataPos] / 255.0f;
                    break;
                case 16:
                    buffer[i] = ((short*)pData)[g_dataPos / 2] / 32768.0f;
                    break;
                case 32:
                    buffer[i] = ((float*)pData)[g_dataPos / 4];
                    break;
                default:
                    return E_FAIL;
                }
                
                g_dataPos += wavFmt.wBitsPerSample / 8;
                break;
            case 2:
                switch (wavFmt.wBitsPerSample) {
                case 8:
                    buffer[i] = (pData[g_dataPos] + pData[g_dataPos + 1]) / 2.0f / 255.0f;
                    break;
                case 16:
                    buffer[i] = (((short*)pData)[g_dataPos / 2] + ((short*)pData)[g_dataPos / 2 + 1]) / 2.0f / 32768.0f;
                    break;
                case 32:
                    buffer[i] = (((float*)pData)[g_dataPos / 4] + ((float*)pData)[g_dataPos / 4 + 1]) / 2.0f;
                    break;
                default:
                    return E_FAIL;
                }
                
                g_dataPos += wavFmt.wBitsPerSample * 2 / 8;
                break;
            default:
                return E_FAIL;
            }
        }
        else
            buffer[i] = 0;
    }
    return S_OK;
}

FFTUtil::calc 函数

时域到频域的计算,具体 FFT 算法网上可以找到很多,比如 KISS FFT

void FFTUtil::calc(IN float* pSrc, int srcLen, OUT double** ppResult, OUT int& window)
{
    int m = getNearestM(srcLen);
    window = 1 << m;

    complex<double>* input = new complex<double>[window];
    for (int i = 0; i < srcLen; ++i)
        input[i] = pSrc[i];

    for (int i = srcLen; i < window; ++i)
        input[i] = 0;

    double wndSum = hammingWindow(input, window);
    *ppResult = new double[window];

    fft(m, input, false);
    // Scale the magnitude of FFT by window and factor of 2, because we are using half of FFT spectrum
    for (int i = 0; i < window; ++i)
        (*ppResult)[i] = sqrt(pow(input[i].real(), 2) + pow(input[i].imag(), 2)) * 2 / wndSum;
    delete[] input;
}

按指定频率计算对应的幅值

PLOT_FREQ 指定了频率分割,两个频率之间取平均幅值,以分贝(dB)显示。

const double PLOT_FREQ[] = { 10, 30, 50, 70, 100, 150, 200, 400, 700, 1000, 2000, 
                             3000, 4000, 5000, 7000, 10000, 20000 }; // Hz

for (int i = 1; i < _countof(PLOT_FREQ); ++i) {
    int idxStart = (int)(PLOT_FREQ[i - 1] * window / wavFmt.nSamplesPerSec);
    int idxEnd = (int)(PLOT_FREQ[i] * window / wavFmt.nSamplesPerSec);
    if (idxStart > idxEnd || idxEnd >= window)
        continue;

    AUDIO_SPECTRUM spec = {0};
    spec.freqStart = PLOT_FREQ[i - 1];
    spec.freqEnd = PLOT_FREQ[i];
    
    // use the average amplitude
    double ampSum = 0;
    for (int j = idxStart; j <= idxEnd; ++j)
        ampSum += pResult[j];
    spec.amplitude = 20 * log10(ampSum / (idxEnd - idxStart));
    spectrum.push_back(spec);
}

Sample 程序展示

横轴为频率,纵轴为幅值,用 D2D 渐变色渲染。audio spectrum
Blueware
EOF

  • 6
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值