音频频谱 via FFT
- 频谱和均衡器
- 声音信号的时域和频域
- FFT
- AudioSpectrum sample
- 工作流程
- 源代码
- _readAudioData 函数
- FFTUtil::calc 函数
- 按指定频率计算对应的幅值
- Sample 程序展示
频谱和均衡器
频谱和均衡器,几乎是媒体播放程序的必备物件,没有这两个功能的媒体播放程序会被认为不够专业。
声音信号的时域和频域
- 时域
是描述数学函数或物理信号对时间的关系。例如一个信号的时域波形可以表达信号随着时间的变化。 - 频域
是指在对函数或信号进行分析时,分析其和频率有关的部份,而不是和时间有关的部份。例如傅里叶变换可以将一个时域信号转换成在不同频率下对应的振幅及相位,其频谱就是时域信号在频域下的表现,而反傅里叶变换可以将频谱再转换回时域的信号。
或者到 这里 体验一下不同频率成分组成的波形。
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 渐变色渲染。
– EOF –